diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..cb0903a --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,90 @@ +{ + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook sessionstart" + } + ] + } + ], + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook userpromptsubmit" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Read", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook pretooluse" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash|Grep|Glob|WebFetch|WebSearch", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook posttooluse" + } + ] + } + ], + "PreCompact": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook precompact" + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook stop" + } + ] + } + ], + "SubagentStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook subagentstart" + } + ] + } + ], + "SubagentStop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook subagentstop" + } + ] + } + ] +} diff --git a/.gemini/commands/goal.toml b/.gemini/commands/goal.toml new file mode 100644 index 0000000..0f17426 --- /dev/null +++ b/.gemini/commands/goal.toml @@ -0,0 +1,12 @@ +description = "Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)" +prompt = """ +Drive the goal in the arguments to done WITH YOU in THIS session (single context). The verify gate is the truth, not your judgment. +1. Treat the arguments as the goal (or a `goal.md` checkbox file if one is named). +2. ORCHESTRATE FIRST — call the knitbrain_run tool with the goal and ADHERE to its verdict: adopt the returned SKILL (refine it, then knitbrain_skill_save); if it proposes agents, spawn them via your host's sub-agent mechanism and coordinate on knitbrain_team_post. +3. Pick the verify command by precedence: an explicit --verify > the goal file's `VERIFY:` line > `npm test` when a package.json exists. If none is derivable, ASK the user for the gate — do NOT invent a command that passes. +4. Read an optional --for <30m|1h> and convert it to deadline_ms. +5. Drive the knitbrain_run_loop tool each cycle with { goal, verify_cmd, max_iters, deadline_ms }: make the smallest real fix, then call knitbrain_run_loop again with the SAME goal so iteration + the time budget carry across calls. +6. NEVER fake met=true. Stop only at a real met=true, OR max_iters, OR the --for deadline, then report the honest final state (what passed, what is still open) and close the loop: knitbrain_record_learning + knitbrain_skill_outcome. + +For a HANDS-OFF autonomous run (a FRESH agent per iteration, in the background), use /loop instead — it launches the external runner (knitbrain loop). +""" diff --git a/.gemini/commands/loop.toml b/.gemini/commands/loop.toml new file mode 100644 index 0000000..46d7234 --- /dev/null +++ b/.gemini/commands/loop.toml @@ -0,0 +1,9 @@ +description = "Launch the autonomous external runner (knitbrain loop) in the background" +prompt = """ +Launch knitbrain's EXTERNAL runner in the BACKGROUND, then report the handle. Do NOT run the loop inline — it would block this session. +1. Resolve a goal file from the arguments: a markdown file with `- [ ] task` checkboxes (default `goal.md`). If it is missing, tell the user to run `knitbrain onboard` first and stop — do NOT invent tasks. +2. Using your terminal tool, run DETACHED (pass through any --for/--max/--verify/--reviewer the user gave): + nohup knitbrain loop --for 1h --agent "gemini -p" > .loop.log 2>&1 & echo "loop PID $!" +3. Report the PID + log path (`.loop.log`). The runner spawns a FRESH "gemini -p" per `- [ ]` task, runs the verify gate, and ticks `- [x]` only on green. Watch the boxes tick, tail the log, or run `knitbrain dashboard`. Stop early with `kill `. +4. Do NOT tick any box yourself — only the runner's verify (and optional reviewer) gate marks a task done. No false green. +""" diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 0000000..a6753f0 --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,59 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook sessionstart" + } + ] + } + ], + "BeforeTool": [ + { + "matcher": "Read", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook pretooluse" + } + ] + } + ], + "AfterTool": [ + { + "matcher": "Bash|Grep|Glob|WebFetch|WebSearch", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook posttooluse" + } + ] + } + ], + "PreCompress": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook precompact" + } + ] + } + ], + "AfterAgent": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "knitbrain-hook stop" + } + ] + } + ] + } +} diff --git a/.github/prompts/goal.prompt.md b/.github/prompts/goal.prompt.md new file mode 100644 index 0000000..c0c95c7 --- /dev/null +++ b/.github/prompts/goal.prompt.md @@ -0,0 +1,13 @@ +--- +description: Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration) +--- + +Drive the goal in the arguments to done WITH YOU in THIS session (single context). The verify gate is the truth, not your judgment. +1. Treat the arguments as the goal (or a `goal.md` checkbox file if one is named). +2. ORCHESTRATE FIRST — call the knitbrain_run tool with the goal and ADHERE to its verdict: adopt the returned SKILL (refine it, then knitbrain_skill_save); if it proposes agents, spawn them via your host's sub-agent mechanism and coordinate on knitbrain_team_post. +3. Pick the verify command by precedence: an explicit --verify > the goal file's `VERIFY:` line > `npm test` when a package.json exists. If none is derivable, ASK the user for the gate — do NOT invent a command that passes. +4. Read an optional --for <30m|1h> and convert it to deadline_ms. +5. Drive the knitbrain_run_loop tool each cycle with { goal, verify_cmd, max_iters, deadline_ms }: make the smallest real fix, then call knitbrain_run_loop again with the SAME goal so iteration + the time budget carry across calls. +6. NEVER fake met=true. Stop only at a real met=true, OR max_iters, OR the --for deadline, then report the honest final state (what passed, what is still open) and close the loop: knitbrain_record_learning + knitbrain_skill_outcome. + +For a HANDS-OFF autonomous run (a FRESH agent per iteration, in the background), use /loop instead — it launches the external runner (knitbrain loop). diff --git a/.github/prompts/loop.prompt.md b/.github/prompts/loop.prompt.md new file mode 100644 index 0000000..6477b36 --- /dev/null +++ b/.github/prompts/loop.prompt.md @@ -0,0 +1,10 @@ +--- +description: Launch the autonomous external runner (knitbrain loop) in the background +--- + +Launch knitbrain's EXTERNAL runner in the BACKGROUND, then report the handle. Do NOT run the loop inline — it would block this session. +1. Resolve a goal file from the arguments: a markdown file with `- [ ] task` checkboxes (default `goal.md`). If it is missing, tell the user to run `knitbrain onboard` first and stop — do NOT invent tasks. +2. Using your terminal tool, run DETACHED (pass through any --for/--max/--verify/--reviewer the user gave): + nohup knitbrain loop --for 1h --agent "claude -p" > .loop.log 2>&1 & echo "loop PID $!" +3. Report the PID + log path (`.loop.log`). The runner spawns a FRESH "claude -p" per `- [ ]` task, runs the verify gate, and ticks `- [x]` only on green. Watch the boxes tick, tail the log, or run `knitbrain dashboard`. Stop early with `kill `. +4. Do NOT tick any box yourself — only the runner's verify (and optional reviewer) gate marks a task done. No false green. diff --git a/README.md b/README.md index a1a5574..8ccbe85 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,10 @@ One engine, three ways in — the headless loop is the front door: passes; questions get answered directly. - **Two slash front doors, on every host that has a slash surface** (Claude Code, Codex, Gemini, VS Code Copilot, Windsurf — Cursor via terminal): - - **`/goal `** drives the gate **with you, in this session** — `knitbrain_run` + - **`/goal-knitbrain `** drives the gate **with you, in this session** — `knitbrain_run` orchestrates a skill + agents, then `knitbrain_run_loop` runs your verify command each cycle until it exits 0. Single context, interactive. - - **`/loop goal.md --for 2h`** hands off to the **external runner** (`knitbrain loop`): it + - **`/loop-knitbrain goal.md --for 2h`** hands off to the **external runner** (`knitbrain loop`): it *launches detached*, spawns a fresh agent per checkbox, and owns the loop itself — surviving your context window, not depending on any model choosing to continue. A slash command can't *be* an hour-long loop, so it launches the runner and hands back a watch handle. @@ -132,7 +132,7 @@ alert on 1. ```cron # weekdays 9am: drive the goal for up to 2h, lint as the independent reviewer -0 9 * * 1-5 cd /path/to/repo && knitbrain loop goal.md --for 2h --reviewer "npm run lint" >> ~/.knitbrain/loop.log 2>&1 +0 9 * * 1-5 cd /path/to/repo && knitbrain loop goal.md --for 2h --reviewer "npm run lint" >> ~/.knitbrain/loop-knitbrain.log 2>&1 ``` Same one-liner works as a launchd `ProgramArguments`, a CI cron job step, or the command behind @@ -196,19 +196,19 @@ The optimizer is identical everywhere; what differs is reach: | Platform | MCP tools | Hook enforcement | Auto-compression | Slash commands | |---|---|---|---|---| -| Claude Code | ✅ | ✅ full (deny · stop-block · inject · rewrite) | ✅ hooks | `/goal` `/loop` `/meter` `/handoff` `/terse` (`.claude/commands`) | -| Codex CLI | ✅ | ✅ full (`.codex/hooks.json`) | hooks + `knitbrain_read` | `/goal` `/loop` (`~/.codex/prompts`) | +| Claude Code | ✅ | ✅ full (deny · stop-block · inject · rewrite) | ✅ hooks | `/goal-knitbrain` `/loop-knitbrain` `/meter` `/handoff` `/terse` (`.claude/commands`) | +| Codex CLI | ✅ | ✅ full (`.codex/hooks.json`) | hooks + `knitbrain_read` | `/goal-knitbrain` `/loop-knitbrain` (`~/.codex/prompts`) | | Cursor | ✅ | ✅ deny + follow-up loop (`.cursor/hooks.json`) | hooks + `knitbrain_read` | — (no slash API; documented in rules) | -| Gemini CLI | ✅ | ✅ deny + AfterAgent loop (`.gemini/settings.json`) | hooks + `knitbrain_read` | `/goal` `/loop` (`.gemini/commands/*.toml`) | -| VS Code Copilot | ✅ | ✅ full (reads `.claude/settings.json` natively) | hooks + `knitbrain_read` | `/goal` `/loop` (`.github/prompts/*.prompt.md`) | -| Windsurf | ✅ | ✅ deny-only (exit-2) (`.windsurf/hooks.json`) | hooks + `knitbrain_read` | `/goal` `/loop` (`.windsurf/workflows`) | +| Gemini CLI | ✅ | ✅ deny + AfterAgent loop (`.gemini/settings.json`) | hooks + `knitbrain_read` | `/goal-knitbrain` `/loop-knitbrain` (`.gemini/commands/*.toml`) | +| VS Code Copilot | ✅ | ✅ full (reads `.claude/settings.json` natively) | hooks + `knitbrain_read` | `/goal-knitbrain` `/loop-knitbrain` (`.github/prompts/*.prompt.md`) | +| Windsurf | ✅ | ✅ deny-only (exit-2) (`.windsurf/hooks.json`) | hooks + `knitbrain_read` | `/goal-knitbrain` `/loop-knitbrain` (`.windsurf/workflows`) | | Cline · any other MCP client | ✅ | — (advisory; hooks planned where APIs allow) | via `knitbrain_read` | — (runner works from any terminal) | | Any agent, API key | ✅ | — | ✅ proxy (full wire) | — | One hook binary serves every row: it detects the calling platform from the payload and answers in that host's schema. Where a host's API can't do something (Cursor can't block stop; Gemini can't rewrite output), knitbrain degrades to the nearest honest mechanism instead of claiming otherwise. -`/goal` and `/loop` ship for every host with a slash-command surface — each in that host's native +`/goal-knitbrain` and `/loop-knitbrain` ship for every host with a slash-command surface — each in that host's native format — so both front doors are the same everywhere. Cursor has no such surface; there the runner is a terminal command (`knitbrain loop`), documented in its always-on rules. diff --git a/package.json b/package.json index bbf9a61..2325707 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "knitbrain", - "version": "0.20.0", + "version": "0.20.1", "description": "Loop engineering substrate for coding agents — state, lossless optimization, and hook enforcement: verify-gated loops that can't lie, run away, or go broke. For Claude Code, Codex, Cursor, Gemini CLI, VS Code Copilot, and any MCP client.", "type": "module", "main": "dist/lib.js", diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 66c890d..660690f 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -1089,12 +1089,12 @@ export const TOOLS: readonly ToolDef[] = [ "", ...tasks, "", - "Drive hands-off: `/loop goal.md --for 1h` (autonomous external runner) or `/goal` in this session. CLI: `knitbrain loop goal.md`.", + "Drive hands-off: `/loop-knitbrain goal.md --for 1h` (autonomous external runner) or `/goal-knitbrain` in this session. CLI: `knitbrain loop goal.md`.", ].join("\n"), "utf8", ); const checkboxCount = tasks.filter((t) => t.startsWith("- [ ]")).length; - goalNote = ` goal.md written (${checkboxCount} checkbox${checkboxCount === 1 ? "" : "es"}, VERIFY honored by the loop) — start with \`/loop goal.md --for 1h\` (autonomous runner) or \`/goal\` in-session.`; + goalNote = ` goal.md written (${checkboxCount} checkbox${checkboxCount === 1 ? "" : "es"}, VERIFY honored by the loop) — start with \`/loop-knitbrain goal.md --for 1h\` (autonomous runner) or \`/goal-knitbrain\` in-session.`; } return `Onboarding complete — Project Charter ("${r.page}") + constraints skill ("${r.skill}") + workflow written (ROUTING covers: ${domains.join(", ") || "none"}). knitbrain_load_session now surfaces your intent + workflow every session.${gapHint}${goalNote}`; } diff --git a/src/platforms.ts b/src/platforms.ts index 4ea7def..271d146 100644 --- a/src/platforms.ts +++ b/src/platforms.ts @@ -154,7 +154,7 @@ Terse: "New object ref each render → re-render. Wrap in useMemo."`; * Goal-loop steering (Gap 6 — the thesis: turn any platform into a loop). * Injected every turn by the UserPromptSubmit hook so the DEFAULT stance is * "drive this to a checkable gate", not "answer once" — the loop is opt-out - * (pure questions), not opt-in (/goal). Kept to one line: it costs tokens on + * (pure questions), not opt-in (/goal-knitbrain). Kept to one line: it costs tokens on * every prompt, so it states the stance and defers mechanics to knitbrain_run. */ export const GOAL_LOOP_NUDGE = @@ -209,9 +209,9 @@ export function claudeArtifacts(cfg: SetupConfig): Artifact[] { content: `---\ndescription: Terse knitbrain output (lite|full|ultra) — same facts, fewer output tokens\nargument-hint: [lite|full|ultra]\n---\n\nAdopt terse output for the rest of this session. Run \`knitbrain terse $ARGUMENTS\` and follow the printed guide (default level: full). Say "verbose" to switch off.\n`, }, { - path: ".claude/commands/goal.md", + path: ".claude/commands/goal-knitbrain.md", mode: "write", - content: `---\ndescription: Run the full knitbrain workflow for a goal — orchestrate skill + agents, then drive to a checkable gate until met\nargument-hint: \n---\n\nDrive \`$ARGUMENTS\` to done with the full knitbrain workflow. The gate is the truth, not your judgment.\n\n1. Treat \`$ARGUMENTS\` as the goal. If it names or implies a checkbox goal file (e.g. a \`goal.md\`), use that file; otherwise state the goal inline.\n2. ORCHESTRATE FIRST — call the \`knitbrain_run\` tool with the goal and ADHERE to the verdict:\n - \`autoPlanMode=true\` → enter your host's plan mode and get approval before any edit;\n - adopt the returned SKILL; refine it while working, then \`knitbrain_skill_save\`;\n - if it proposes agents, spawn them via your host's sub-agent mechanism and coordinate on \`knitbrain_team_post\` / \`knitbrain_team_board\`;\n - run the listed host slash-commands when useful.\n3. Pick the verify command by precedence: an explicit \`--verify\` in the args, else the goal file's \`VERIFY:\` line, else \`npm test\` when a package.json exists. If none is derivable, ASK the user for the gate — do NOT invent a command that passes.\n4. Read an optional \`--for \` from the args (e.g. \`30m\`, \`1h\`, \`90s\`) and convert it to milliseconds — that's the wall-clock budget.\n5. Call the \`knitbrain_run_loop\` tool with \`{ goal, verify_cmd, max_iters, deadline_ms }\` (max_iters default 6; omit \`deadline_ms\` when no \`--for\` was given).\n6. Each cycle, follow the returned directive: make the smallest real fix (delegating to the spawned agents where apt), then call \`knitbrain_run_loop\` again with the SAME goal so iteration + the time budget carry across calls.\n7. NEVER fake \`met=true\`. Stop only at a real \`met=true\`, OR \`max_iters\`, OR the \`--for\` deadline (\`stopped:"deadline"\`), then report the honest final state (what passed, what's still open) and close the loop: \`knitbrain_record_learning\` + \`knitbrain_skill_outcome\`.\n\nSibling: \`/loop \` HANDS OFF to the EXTERNAL runner (\`knitbrain loop\`) — a detached background process that spawns a fresh agent per iteration and owns the loop itself, surviving your context window. Use \`/goal\` to drive a gate in THIS session; use \`/loop\` for hands-off autonomous runs.\n`, + content: `---\ndescription: Run the full knitbrain workflow for a goal — orchestrate skill + agents, then drive to a checkable gate until met\nargument-hint: \n---\n\nDrive \`$ARGUMENTS\` to done with the full knitbrain workflow. The gate is the truth, not your judgment.\n\n1. Treat \`$ARGUMENTS\` as the goal. If it names or implies a checkbox goal file (e.g. a \`goal.md\`), use that file; otherwise state the goal inline.\n2. ORCHESTRATE FIRST — call the \`knitbrain_run\` tool with the goal and ADHERE to the verdict:\n - \`autoPlanMode=true\` → enter your host's plan mode and get approval before any edit;\n - adopt the returned SKILL; refine it while working, then \`knitbrain_skill_save\`;\n - if it proposes agents, spawn them via your host's sub-agent mechanism and coordinate on \`knitbrain_team_post\` / \`knitbrain_team_board\`;\n - run the listed host slash-commands when useful.\n3. Pick the verify command by precedence: an explicit \`--verify\` in the args, else the goal file's \`VERIFY:\` line, else \`npm test\` when a package.json exists. If none is derivable, ASK the user for the gate — do NOT invent a command that passes.\n4. Read an optional \`--for \` from the args (e.g. \`30m\`, \`1h\`, \`90s\`) and convert it to milliseconds — that's the wall-clock budget.\n5. Call the \`knitbrain_run_loop\` tool with \`{ goal, verify_cmd, max_iters, deadline_ms }\` (max_iters default 6; omit \`deadline_ms\` when no \`--for\` was given).\n6. Each cycle, follow the returned directive: make the smallest real fix (delegating to the spawned agents where apt), then call \`knitbrain_run_loop\` again with the SAME goal so iteration + the time budget carry across calls.\n7. NEVER fake \`met=true\`. Stop only at a real \`met=true\`, OR \`max_iters\`, OR the \`--for\` deadline (\`stopped:"deadline"\`), then report the honest final state (what passed, what's still open) and close the loop: \`knitbrain_record_learning\` + \`knitbrain_skill_outcome\`.\n\nSibling: \`/loop-knitbrain \` HANDS OFF to the EXTERNAL runner (\`knitbrain loop\`) — a detached background process that spawns a fresh agent per iteration and owns the loop itself, surviving your context window. Use \`/goal-knitbrain\` to drive a gate in THIS session; use \`/loop-knitbrain\` for hands-off autonomous runs.\n`, }, { path: ".claude/rules/knitbrain.md", @@ -221,7 +221,7 @@ export function claudeArtifacts(cfg: SetupConfig): Artifact[] { ]; } -/** Claude Code: /loop command — same engine as /goal, plus an explicit +/** Claude Code: /loop-knitbrain command — same engine as /goal-knitbrain, plus an explicit * --for/--iters budget and self-heal from prior-cycle failures (LoopState.failures, * surfaced automatically by knitbrain_run_loop's directive). Kept OUT of * claudeArtifacts() (applied separately in runSetup) so the artifact-count @@ -229,15 +229,15 @@ export function claudeArtifacts(cfg: SetupConfig): Artifact[] { export function claudeLoopArtifacts(): Artifact[] { return [ { - path: ".claude/commands/loop.md", + path: ".claude/commands/loop-knitbrain.md", mode: "write", - content: `---\ndescription: Launch the autonomous external runner — a detached process that spawns a FRESH agent per iteration until the goal file's boxes are ticked or the time budget elapses\nargument-hint: [--for 1h] [--max N] [--agent "cmd"] [--verify "cmd"]\n---\n\n\`/loop\` LAUNCHES knitbrain's external runner (\`knitbrain loop\`) — a background process that OWNS the loop: for each \`- [ ]\` task in the goal file it spawns a FRESH agent (\`claude -p\` by default), runs the verify gate, ticks \`- [x]\` only on green, and repeats until all boxes are done, \`--max\` iterations, or the \`--for\` budget elapses. It survives your context window (fresh agent each iteration) and does NOT depend on any model choosing to continue — the runner re-invokes. A slash command cannot BE an hour-long loop (that would freeze this chat), so it LAUNCHES the runner detached and hands you a watch handle.\n\n1. Resolve the goal file from \`$ARGUMENTS\`: a path to a markdown file with \`- [ ] task\` checkboxes. If none is given, use \`goal.md\` in the cwd; if that is missing, tell the user to run \`knitbrain onboard\` first (it writes a goal.md) and stop — do NOT invent tasks.\n2. LAUNCH DETACHED so this chat is not frozen for the whole budget. Run in the background: \`nohup knitbrain loop > .loop.log 2>&1 & echo "loop PID $!"\`. Pass \`--for\`, \`--max\`, \`--agent\`, \`--verify\`, \`--reviewer\` straight through from \`$ARGUMENTS\`; the runner picks its gate itself (--verify > the goal file's VERIFY: line > npm test).\n3. Report to the user: the PID, the log path (\`.loop.log\`), and how to watch — \`- [ ]\`→\`- [x]\` boxes tick in the goal file as tasks pass, \`.progress\` appends one line per done task, and \`knitbrain dashboard\` shows the badge. Stop early with \`kill \`.\n4. Do NOT block waiting on it, and do NOT tick any box yourself — the runner's verify (and optional reviewer) gate is the ONLY thing that marks a task done. No false green.\n\nSibling: \`/goal \` drives a gate WITH YOU in THIS session (single context, interactive, you make the fixes). \`/loop\` HANDS OFF to the external runner (fresh context per iteration, autonomous, background).\n`, + content: `---\ndescription: Launch the autonomous external runner — a detached process that spawns a FRESH agent per iteration until the goal file's boxes are ticked or the time budget elapses\nargument-hint: [--for 1h] [--max N] [--agent "cmd"] [--verify "cmd"]\n---\n\n\`/loop-knitbrain\` LAUNCHES knitbrain's external runner (\`knitbrain loop\`) — a background process that OWNS the loop: for each \`- [ ]\` task in the goal file it spawns a FRESH agent (\`claude -p\` by default), runs the verify gate, ticks \`- [x]\` only on green, and repeats until all boxes are done, \`--max\` iterations, or the \`--for\` budget elapses. It survives your context window (fresh agent each iteration) and does NOT depend on any model choosing to continue — the runner re-invokes. A slash command cannot BE an hour-long loop (that would freeze this chat), so it LAUNCHES the runner detached and hands you a watch handle.\n\n1. Resolve the goal file from \`$ARGUMENTS\`: a path to a markdown file with \`- [ ] task\` checkboxes. If none is given, use \`goal.md\` in the cwd; if that is missing, tell the user to run \`knitbrain onboard\` first (it writes a goal.md) and stop — do NOT invent tasks.\n2. LAUNCH DETACHED so this chat is not frozen for the whole budget. Run in the background: \`nohup knitbrain loop > .loop.log 2>&1 & echo "loop PID $!"\`. Pass \`--for\`, \`--max\`, \`--agent\`, \`--verify\`, \`--reviewer\` straight through from \`$ARGUMENTS\`; the runner picks its gate itself (--verify > the goal file's VERIFY: line > npm test).\n3. Report to the user: the PID, the log path (\`.loop.log\`), and how to watch — \`- [ ]\`→\`- [x]\` boxes tick in the goal file as tasks pass, \`.progress\` appends one line per done task, and \`knitbrain dashboard\` shows the badge. Stop early with \`kill \`.\n4. Do NOT block waiting on it, and do NOT tick any box yourself — the runner's verify (and optional reviewer) gate is the ONLY thing that marks a task done. No false green.\n\nSibling: \`/goal-knitbrain \` drives a gate WITH YOU in THIS session (single context, interactive, you make the fixes). \`/loop-knitbrain\` HANDS OFF to the external runner (fresh context per iteration, autonomous, background).\n`, }, ]; } /** - * Body for a platform \`/loop\` command. None of these platforms can BE the + * Body for a platform \`/loop-knitbrain\` command. None of these platforms can BE the * loop (a command runs inside one turn), so each LAUNCHES the external runner * (\`knitbrain loop\`) detached and reports a watch handle. \`workerAgent\` is the * headless CLI the runner spawns per iteration (claude -p / codex exec / @@ -257,10 +257,10 @@ export function loopLaunchInstructions(workerAgent: string): string { } /** - * Body for a platform \`/goal\` command: drive a goal to a verify gate WITH YOU + * Body for a platform \`/goal-knitbrain\` command: drive a goal to a verify gate WITH YOU * in THIS session (single context, in-chat orchestration). Mirror of the Claude * goal.md steps, shared so no platform drifts. For a hands-off autonomous run - * (fresh agent per iteration, background), it points to /loop instead. + * (fresh agent per iteration, background), it points to /loop-knitbrain instead. */ export function goalOrchestrationInstructions(): string { return [ @@ -272,7 +272,7 @@ export function goalOrchestrationInstructions(): string { "5. Drive the knitbrain_run_loop tool each cycle with { goal, verify_cmd, max_iters, deadline_ms }: make the smallest real fix, then call knitbrain_run_loop again with the SAME goal so iteration + the time budget carry across calls.", "6. NEVER fake met=true. Stop only at a real met=true, OR max_iters, OR the --for deadline, then report the honest final state (what passed, what is still open) and close the loop: knitbrain_record_learning + knitbrain_skill_outcome.", "", - "For a HANDS-OFF autonomous run (a FRESH agent per iteration, in the background), use /loop instead — it launches the external runner (knitbrain loop).", + "For a HANDS-OFF autonomous run (a FRESH agent per iteration, in the background), use /loop-knitbrain instead — it launches the external runner (knitbrain loop).", ].join("\n"); } @@ -285,7 +285,7 @@ export function cursorArtifacts(): Artifact[] { { path: ".cursor/rules/knitbrain.mdc", mode: "write", - content: `---\ndescription: Knit Brain memory + token optimization\nalwaysApply: true\n---\n\n${NOTATION_GUIDE}\n\n${TERSE_MODE}\n\nAutonomous loop (Cursor has no custom slash commands): run the external runner in your terminal — \`knitbrain loop goal.md --for 1h --agent "claude -p"\` (spawns a fresh worker per \`- [ ]\` task, verify-gated, ticks \`- [x]\` only on green).\n\nGoal orchestration (no slash on Cursor): just state the task — call knitbrain_run first, adopt the returned skill, then drive knitbrain_run_loop to a verify gate in THIS session (same as /goal elsewhere).\n`, + content: `---\ndescription: Knit Brain memory + token optimization\nalwaysApply: true\n---\n\n${NOTATION_GUIDE}\n\n${TERSE_MODE}\n\nAutonomous loop (Cursor has no custom slash commands): run the external runner in your terminal — \`knitbrain loop goal.md --for 1h --agent "claude -p"\` (spawns a fresh worker per \`- [ ]\` task, verify-gated, ticks \`- [x]\` only on green).\n\nGoal orchestration (no slash on Cursor): just state the task — call knitbrain_run first, adopt the returned skill, then drive knitbrain_run_loop to a verify gate in THIS session (same as /goal-knitbrain elsewhere).\n`, }, ]; } @@ -305,14 +305,14 @@ export function geminiArtifacts(): Artifact[] { return [ { path: ".gemini/settings.json", content: "", mode: "json-merge-hooks", hooksData: GEMINI_HOOKS, hooksWrapped: true }, { - path: ".gemini/commands/loop.toml", + path: ".gemini/commands/loop-knitbrain.toml", mode: "write", content: 'description = "Launch the autonomous external runner (knitbrain loop) in the background"\n' + 'prompt = """\n' + loopLaunchInstructions("gemini -p") + '\n"""\n', }, { - path: ".gemini/commands/goal.toml", + path: ".gemini/commands/goal-knitbrain.toml", mode: "write", content: 'description = "Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)"\n' + @@ -341,14 +341,14 @@ export function vscodeArtifacts(): Artifact[] { content: `---\napplyTo: "**"\n---\n\n${NOTATION_GUIDE}\n\n${TERSE_MODE}\n`, }, { - path: ".github/prompts/loop.prompt.md", + path: ".github/prompts/loop-knitbrain.prompt.md", mode: "write", content: "---\ndescription: Launch the autonomous external runner (knitbrain loop) in the background\n---\n\n" + loopLaunchInstructions("claude -p") + "\n", }, { - path: ".github/prompts/goal.prompt.md", + path: ".github/prompts/goal-knitbrain.prompt.md", mode: "write", content: "---\ndescription: Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)\n---\n\n" + @@ -369,17 +369,17 @@ export function windsurfArtifacts(): Artifact[] { // shared with Cursor's merge logic (dedupe by command, hooks:{...} wrapper). { path: ".windsurf/hooks.json", content: "", mode: "json-merge-cursor-hooks", flatHooksData: WINDSURF_HOOKS }, { - path: ".windsurf/workflows/loop.md", + path: ".windsurf/workflows/loop-knitbrain.md", mode: "write", content: - "---\nname: loop\ndescription: Launch the autonomous external runner (knitbrain loop) in the background\n---\n\n## Steps\n" + + "---\nname: loop-knitbrain\ndescription: Launch the autonomous external runner (knitbrain loop) in the background\n---\n\n## Steps\n" + loopLaunchInstructions("claude -p") + "\n", }, { - path: ".windsurf/workflows/goal.md", + path: ".windsurf/workflows/goal-knitbrain.md", mode: "write", content: - "---\nname: goal\ndescription: Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)\n---\n\n## Steps\n" + + "---\nname: goal-knitbrain\ndescription: Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)\n---\n\n## Steps\n" + goalOrchestrationInstructions() + "\n", }, ]; @@ -411,12 +411,12 @@ export function codexSnippet(cfg: SetupConfig): string { "# Optional proxy (Codex supports base-URL override):", `# export OPENAI_BASE_URL=${cfg.proxyEnv["OPENAI_BASE_URL"]}`, "", - "# Autonomous loop as a /loop slash command (Codex prompts are GLOBAL):", - "# create ~/.codex/prompts/loop.md with front-matter + this body, then type /loop:", + "# Autonomous loop as a /loop-knitbrain slash command (Codex prompts are GLOBAL):", + "# create ~/.codex/prompts/loop-knitbrain.md with front-matter + this body, then type /loop-knitbrain:", "# " + loopLaunchInstructions("codex exec").replace(/\n/g, "\n# "), "", - "# In-session goal orchestration as a /goal slash command (also global):", - "# create ~/.codex/prompts/goal.md with front-matter + this body, then type /goal:", + "# In-session goal orchestration as a /goal-knitbrain slash command (also global):", + "# create ~/.codex/prompts/goal-knitbrain.md with front-matter + this body, then type /goal-knitbrain:", "# " + goalOrchestrationInstructions().replace(/\n/g, "\n# "), ].join("\n"); } @@ -433,8 +433,8 @@ export function slashCommands(platform: string): Array<{ cmd: string; when: stri { cmd: "/handoff", when: "save session handoff before clearing" }, { cmd: "/clear", when: "after handoff saved — start fresh window" }, { cmd: "/compact", when: "mid-task when window warn but can't clear yet" }, - { cmd: "/goal", when: "drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)" }, - { cmd: "/loop", when: "launch the autonomous external runner (knitbrain loop) in the background" }, + { cmd: "/goal-knitbrain", when: "drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)" }, + { cmd: "/loop-knitbrain", when: "launch the autonomous external runner (knitbrain loop) in the background" }, ]; } if (platform === "cursor") { @@ -442,8 +442,8 @@ export function slashCommands(platform: string): Array<{ cmd: string; when: stri } if (platform === "gemini" || platform === "vscode" || platform === "windsurf" || platform === "codex") { return [ - { cmd: "/goal", when: "drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)" }, - { cmd: "/loop", when: "launch the autonomous external runner (knitbrain loop) in the background" }, + { cmd: "/goal-knitbrain", when: "drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)" }, + { cmd: "/loop-knitbrain", when: "launch the autonomous external runner (knitbrain loop) in the background" }, ]; } return []; diff --git a/src/version.ts b/src/version.ts index ed81662..6b72b5c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ /** Single source of truth for server identity. */ -export const VERSION = "0.20.0"; +export const VERSION = "0.20.1"; export const SERVER_NAME = "knitbrain"; diff --git a/tests/audit-fixes.test.ts b/tests/audit-fixes.test.ts index 4554e80..ada8267 100644 --- a/tests/audit-fixes.test.ts +++ b/tests/audit-fixes.test.ts @@ -67,11 +67,11 @@ describe("audit fixes — 0.16.0", () => { it("M8: setup backs up a differing user-edited 'write' file to .bak before overwriting", () => { const cfg = generateConfig(); - const goalArt = claudeArtifacts(cfg).find((a) => a.path === ".claude/commands/goal.md")!; + const goalArt = claudeArtifacts(cfg).find((a) => a.path === ".claude/commands/goal-knitbrain.md")!; // seed a user-edited version const arts: Artifact[] = [goalArt]; applyArtifacts(root, arts, cfg); // first write - const goalPath = join(root, ".claude/commands/goal.md"); + const goalPath = join(root, ".claude/commands/goal-knitbrain.md"); writeFileSync(goalPath, "# my custom goal command\n", "utf8"); applyArtifacts(root, arts, cfg); // re-run setup expect(existsSync(`${goalPath}.bak`)).toBe(true); diff --git a/tests/platforms.test.ts b/tests/platforms.test.ts index d959de4..55e2347 100644 --- a/tests/platforms.test.ts +++ b/tests/platforms.test.ts @@ -29,12 +29,12 @@ describe("platform adapter matrix (rung 16)", () => { expect(paths).toContain(".mcp.json"); expect(paths).toContain(".claude/commands/meter.md"); expect(paths).toContain(".claude/commands/handoff.md"); - expect(paths).toContain(".claude/commands/goal.md"); + expect(paths).toContain(".claude/commands/goal-knitbrain.md"); expect(paths).toContain(".claude/rules/knitbrain.md"); }); - it("/goal orchestrates the full workflow (run -> agents -> loop), not just the thin loop", () => { - const goal = claudeArtifacts(cfg).find((a) => a.path === ".claude/commands/goal.md")!; + it("/goal-knitbrain orchestrates the full workflow (run -> agents -> loop), not just the thin loop", () => { + const goal = claudeArtifacts(cfg).find((a) => a.path === ".claude/commands/goal-knitbrain.md")!; expect(goal.content).toContain("knitbrain_run"); // orchestrate first (classify + skill + agents) expect(goal.content).toContain("knitbrain_run_loop"); // still gated by the verify loop expect(goal.content).toContain("proposes agents"); // agent fan-out wording @@ -108,7 +108,7 @@ describe("platform adapter matrix (rung 16)", () => { const root = mkdtempSync(join(tmpdir(), "knitbrain-plat-")); try { const written = applyArtifacts(root, claudeArtifacts(cfg), cfg); - expect(written.length).toBe(7); // .mcp.json + settings.json(hooks) + 4 commands (meter/handoff/terse/goal) + rules + expect(written.length).toBe(7); // .mcp.json + settings.json(hooks) + 4 commands (meter/handoff/terse/goal-knitbrain) + rules for (const p of written) expect(existsSync(join(root, p))).toBe(true); } finally { rmSync(root, { recursive: true, force: true }); @@ -176,23 +176,23 @@ describe("cross-platform hook config emitters (Tier-1.1 — merge, never clobber } }); - it("claudeLoopArtifacts writes .claude/commands/loop.md, cross-referencing /goal", () => { - const loop = claudeLoopArtifacts().find((a) => a.path === ".claude/commands/loop.md")!; + it("claudeLoopArtifacts writes .claude/commands/loop-knitbrain.md, cross-referencing /goal-knitbrain", () => { + const loop = claudeLoopArtifacts().find((a) => a.path === ".claude/commands/loop-knitbrain.md")!; expect(loop).toBeDefined(); - expect(loop.content).toContain("/goal"); + expect(loop.content).toContain("/goal-knitbrain"); }); - it("goal.md content references /loop (explicit --for/--iters budget escape hatch)", () => { - const goal = claudeArtifacts(cfg).find((a) => a.path === ".claude/commands/goal.md")!; - expect(goal.content).toContain("/loop"); + it("goal.md content references /loop-knitbrain (explicit --for/--iters budget escape hatch)", () => { + const goal = claudeArtifacts(cfg).find((a) => a.path === ".claude/commands/goal-knitbrain.md")!; + expect(goal.content).toContain("/loop-knitbrain"); }); - it("slashCommands('claude-code') includes /loop", () => { + it("slashCommands('claude-code') includes /loop-knitbrain", () => { const cmds = slashCommands("claude-code").map((c) => c.cmd); - expect(cmds).toContain("/loop"); + expect(cmds).toContain("/loop-knitbrain"); }); - // Cross-platform /loop: every platform's command LAUNCHES the external runner + // Cross-platform /loop-knitbrain: every platform's command LAUNCHES the external runner // (knitbrain loop) detached — none runs the loop inline. Verified formats: // Gemini .toml, VS Code .prompt.md, Windsurf .windsurf/workflows/*.md. it("loopLaunchInstructions LAUNCHES the runner in the background, never ticks boxes itself", () => { @@ -203,8 +203,8 @@ describe("cross-platform hook config emitters (Tier-1.1 — merge, never clobber expect(body).toContain("Do NOT tick any box yourself"); // no false green }); - it("Gemini emits .gemini/commands/loop.toml (TOML: description + prompt, gemini -p worker)", () => { - const t = geminiArtifacts().find((a) => a.path === ".gemini/commands/loop.toml")!; + it("Gemini emits .gemini/commands/loop-knitbrain.toml (TOML: description + prompt, gemini -p worker)", () => { + const t = geminiArtifacts().find((a) => a.path === ".gemini/commands/loop-knitbrain.toml")!; expect(t).toBeDefined(); expect(t.content).toContain("description ="); expect(t.content).toContain('prompt = """'); @@ -212,17 +212,17 @@ describe("cross-platform hook config emitters (Tier-1.1 — merge, never clobber expect(t.content).toContain('--agent "gemini -p"'); }); - it("VS Code emits .github/prompts/loop.prompt.md (YAML frontmatter + body)", () => { - const p = vscodeArtifacts().find((a) => a.path === ".github/prompts/loop.prompt.md")!; + it("VS Code emits .github/prompts/loop-knitbrain.prompt.md (YAML frontmatter + body)", () => { + const p = vscodeArtifacts().find((a) => a.path === ".github/prompts/loop-knitbrain.prompt.md")!; expect(p).toBeDefined(); expect(p.content.startsWith("---\ndescription:")).toBe(true); expect(p.content).toContain("nohup knitbrain loop"); }); - it("Windsurf emits .windsurf/workflows/loop.md (name/description frontmatter + ## Steps)", () => { - const w = windsurfArtifacts().find((a) => a.path === ".windsurf/workflows/loop.md")!; + it("Windsurf emits .windsurf/workflows/loop-knitbrain.md (name/description frontmatter + ## Steps)", () => { + const w = windsurfArtifacts().find((a) => a.path === ".windsurf/workflows/loop-knitbrain.md")!; expect(w).toBeDefined(); - expect(w.content).toContain("name: loop"); + expect(w.content).toContain("name: loop-knitbrain"); expect(w.content).toContain("## Steps"); expect(w.content).toContain("knitbrain loop"); }); @@ -232,42 +232,42 @@ describe("cross-platform hook config emitters (Tier-1.1 — merge, never clobber expect(rules.content).toContain("knitbrain loop goal.md"); }); - it("Codex snippet documents the global /loop prompt (codex exec worker)", () => { + it("Codex snippet documents the global /loop-knitbrain prompt (codex exec worker)", () => { const snip = codexSnippet(cfg); - expect(snip).toContain("~/.codex/prompts/loop.md"); + expect(snip).toContain("~/.codex/prompts/loop-knitbrain.md"); expect(snip).toContain("codex exec"); }); - it("slashCommands includes /loop on gemini, vscode, windsurf, codex", () => { + it("slashCommands includes /loop-knitbrain on gemini, vscode, windsurf, codex", () => { for (const p of ["gemini", "vscode", "windsurf", "codex"]) { - expect(slashCommands(p).map((c) => c.cmd)).toContain("/loop"); + expect(slashCommands(p).map((c) => c.cmd)).toContain("/loop-knitbrain"); } }); - // /goal parity: in-session orchestration front door, on every platform /loop - // reaches (no gaps). Distinct from /loop (external runner) — drives the gate + // /goal-knitbrain parity: in-session orchestration front door, on every platform /loop-knitbrain + // reaches (no gaps). Distinct from /loop-knitbrain (external runner) — drives the gate // WITH YOU in-session via knitbrain_run + knitbrain_run_loop. - it("goalOrchestrationInstructions orchestrates in-session and points to /loop for hands-off", () => { + it("goalOrchestrationInstructions orchestrates in-session and points to /loop-knitbrain for hands-off", () => { const body = goalOrchestrationInstructions(); expect(body).toContain("knitbrain_run"); expect(body).toContain("knitbrain_run_loop"); expect(body).toContain("deadline_ms"); expect(body).toContain("NEVER fake met=true"); - expect(body).toContain("/loop"); // cross-reference to the external runner + expect(body).toContain("/loop-knitbrain"); // cross-reference to the external runner }); - it("every /loop platform also emits a /goal command (no gap)", () => { - expect(geminiArtifacts().some((a) => a.path === ".gemini/commands/goal.toml")).toBe(true); - expect(vscodeArtifacts().some((a) => a.path === ".github/prompts/goal.prompt.md")).toBe(true); - expect(windsurfArtifacts().some((a) => a.path === ".windsurf/workflows/goal.md")).toBe(true); - expect(codexSnippet(cfg)).toContain("~/.codex/prompts/goal.md"); + it("every /loop-knitbrain platform also emits a /goal-knitbrain command (no gap)", () => { + expect(geminiArtifacts().some((a) => a.path === ".gemini/commands/goal-knitbrain.toml")).toBe(true); + expect(vscodeArtifacts().some((a) => a.path === ".github/prompts/goal-knitbrain.prompt.md")).toBe(true); + expect(windsurfArtifacts().some((a) => a.path === ".windsurf/workflows/goal-knitbrain.md")).toBe(true); + expect(codexSnippet(cfg)).toContain("~/.codex/prompts/goal-knitbrain.md"); }); - it("slashCommands lists /goal AND /loop on claude-code + gemini + vscode + windsurf + codex", () => { + it("slashCommands lists /goal-knitbrain AND /loop-knitbrain on claude-code + gemini + vscode + windsurf + codex", () => { for (const p of ["claude-code", "gemini", "vscode", "windsurf", "codex"]) { const cmds = slashCommands(p).map((c) => c.cmd); - expect(cmds).toContain("/goal"); - expect(cmds).toContain("/loop"); + expect(cmds).toContain("/goal-knitbrain"); + expect(cmds).toContain("/loop-knitbrain"); } });