From 4711889512d051dbb1b8ba07ba787ed9c91d9141 Mon Sep 17 00:00:00 2001 From: PDgit12 Date: Wed, 8 Jul 2026 12:17:27 +0530 Subject: [PATCH] feat(loop): cross-platform /goal + /loop, external-runner wiring, v0.20.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewire /loop to LAUNCH the external runner (knitbrain loop) detached instead of the in-agent run_loop tool, and bring both front doors to full parity on every host with a slash surface: - /loop: background-launch knitbrain loop (owns control flow, fresh agent per iteration, survives context window) — Claude, Codex, Gemini, VS Code, Windsurf; Cursor via terminal (documented in rules). - /goal: in-session gate orchestration (knitbrain_run -> knitbrain_run_loop), now emitted for all slash-capable platforms via a shared goalOrchestrationInstructions() helper (was Claude-only). - Per-platform native formats: Gemini .gemini/commands/*.toml, VS Code .github/prompts/*.prompt.md, Windsurf .windsurf/workflows/*.md, Codex ~/.codex/prompts (snippet). Formats verified against live docs. - slashCommands() registers /goal + /loop on all five slash platforms (claude-code was missing /goal in the registry). - Onboarding hints + README platform table updated to the honest split. - +7 tests (618 green), consistency + bench pass. Claude-Session: https://claude.ai/code/session_01Mnwx3jqRivG66r1aneoZYf --- README.md | 28 +++++++---- package.json | 2 +- src/mcp/tools.ts | 4 +- src/platforms.ts | 105 ++++++++++++++++++++++++++++++++++++++-- src/version.ts | 2 +- tests/platforms.test.ts | 81 +++++++++++++++++++++++++++++++ 6 files changed, 205 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 15ae21c..a1a5574 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,15 @@ One engine, three ways in — the headless loop is the front door: - **Ambient** (after onboarding): say what you're working on; the injected frame classifies it — actionable requests become goals driven through `knitbrain_run_loop` until the verify gate passes; questions get answered directly. -- **`/goal `** — loop until met. **`/loop --for 2h --iters N`** — same engine - plus a wall-clock and iteration budget. Every iteration runs as a full goal cycle. +- **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` + 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 + *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. **Self-healing:** each failed cycle's verify output is persisted (`failures[]`, last 3) and injected into the next directive — "previous failures — iter 1: …. Address the ROOT CAUSE" — so @@ -189,18 +196,21 @@ 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` | -| Codex CLI | ✅ | ✅ full (`.codex/hooks.json`) | hooks + `knitbrain_read` | — | -| Cursor | ✅ | ✅ deny + follow-up loop (`.cursor/hooks.json`) | hooks + `knitbrain_read` | — | -| Gemini CLI | ✅ | ✅ deny + AfterAgent loop (`.gemini/settings.json`) | hooks + `knitbrain_read` | — | -| VS Code Copilot | ✅ | ✅ full (reads `.claude/settings.json` natively) | hooks + `knitbrain_read` | — | -| Windsurf | ✅ | ✅ deny-only (exit-2) (`.windsurf/hooks.json`) | hooks + `knitbrain_read` | — | -| Cline · any other MCP client | ✅ | — (advisory; hooks planned where APIs allow) | via `knitbrain_read` | — | +| 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`) | +| 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`) | +| 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 +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. ## Commands diff --git a/package.json b/package.json index b48ae7e..bbf9a61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "knitbrain", - "version": "0.19.0", + "version": "0.20.0", "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 ea8b9a3..66c890d 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -1089,12 +1089,12 @@ export const TOOLS: readonly ToolDef[] = [ "", ...tasks, "", - "Drive: knitbrain_run_loop(goal, verify_cmd) per cycle, or `knitbrain loop goal.md`.", + "Drive hands-off: `/loop goal.md --for 1h` (autonomous external runner) or `/goal` 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 \`knitbrain loop goal.md\` or knitbrain_run_loop.`; + 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.`; } 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 a8fe63d..4ea7def 100644 --- a/src/platforms.ts +++ b/src/platforms.ts @@ -211,7 +211,7 @@ export function claudeArtifacts(cfg: SetupConfig): Artifact[] { { path: ".claude/commands/goal.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\` runs this same cycle under an explicit \`--for\`/\`--iters\` budget for autonomous multi-iteration runs (goal = loop until met; loop = goal + time/iteration budget).\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 \` 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`, }, { path: ".claude/rules/knitbrain.md", @@ -231,11 +231,51 @@ export function claudeLoopArtifacts(): Artifact[] { { path: ".claude/commands/loop.md", mode: "write", - content: `---\ndescription: Run a goal as an autonomous multi-iteration loop with an explicit time/iteration budget\nargument-hint: --for --iters N\n---\n\nSibling of \`/goal\` (goal = loop until met; loop = goal + explicit time/iteration budget). Same engine, same rules:\n\n1. Follow \`/goal\`'s steps 1-3 (orchestrate via \`knitbrain_run\`, ADHERE to the verdict, pick the verify command by the same precedence).\n2. Parse \`--for \` (e.g. \`30m\`, \`1h\`) → \`deadline_ms\`, and \`--iters N\` → \`max_iters\` (default 6) from \`$ARGUMENTS\`.\n3. Call \`knitbrain_run_loop\` each cycle with \`{ goal, verify_cmd, max_iters, deadline_ms }\`. If the response's \`directive\` opens with "previous failures", treat those as ROOT CAUSES to fix — do not repeat the same dead-end fix.\n4. Every iteration is a full goal cycle: make the smallest real fix, then call \`knitbrain_run_loop\` again with the SAME goal so iteration + budget carry across calls.\n5. NEVER fake \`met=true\`. Stop only at real \`met=true\`, OR \`max_iters\`, OR the \`--for\` deadline, then report the honest final state and close the loop: \`knitbrain_record_learning\` + \`knitbrain_skill_outcome\`.\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\` 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`, }, ]; } +/** + * Body for a platform \`/loop\` 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 / + * gemini -p). IDE-embedded hosts (VS Code, Windsurf, Cursor) have no headless + * self → they drive an installed CLI worker (default claude -p; override with + * --agent). Uniform text keeps the loop honest everywhere. + */ +export function loopLaunchInstructions(workerAgent: string): string { + return [ + "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 \"" + workerAgent + "\" > .loop.log 2>&1 & echo \"loop PID $!\"", + "3. Report the PID + log path (`.loop.log`). The runner spawns a FRESH \"" + workerAgent + "\" 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.", + ].join("\n"); +} + +/** + * Body for a platform \`/goal\` 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. + */ +export function goalOrchestrationInstructions(): string { + return [ + "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).", + ].join("\n"); +} + /** Cursor: .cursor/mcp.json + hooks.json (Layer 2 enforcement) + an * always-on rules file. */ export function cursorArtifacts(): Artifact[] { @@ -245,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`, + 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`, }, ]; } @@ -264,6 +304,20 @@ export function codexArtifacts(): Artifact[] { export function geminiArtifacts(): Artifact[] { return [ { path: ".gemini/settings.json", content: "", mode: "json-merge-hooks", hooksData: GEMINI_HOOKS, hooksWrapped: true }, + { + path: ".gemini/commands/loop.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", + mode: "write", + content: + 'description = "Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)"\n' + + 'prompt = """\n' + goalOrchestrationInstructions() + '\n"""\n', + }, ]; } @@ -286,6 +340,20 @@ export function vscodeArtifacts(): Artifact[] { mode: "write", content: `---\napplyTo: "**"\n---\n\n${NOTATION_GUIDE}\n\n${TERSE_MODE}\n`, }, + { + path: ".github/prompts/loop.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", + mode: "write", + content: + "---\ndescription: Drive a goal to a verify gate WITH YOU in this session (in-chat orchestration)\n---\n\n" + + goalOrchestrationInstructions() + "\n", + }, ]; } @@ -300,6 +368,20 @@ export function windsurfArtifacts(): Artifact[] { // Layer 2 enforcement: deny-only (exit-2) surface, flat {command} schema // 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", + mode: "write", + content: + "---\nname: loop\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", + 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" + + goalOrchestrationInstructions() + "\n", + }, ]; } @@ -328,6 +410,14 @@ 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:", + "# " + 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:", + "# " + goalOrchestrationInstructions().replace(/\n/g, "\n# "), ].join("\n"); } @@ -343,12 +433,19 @@ 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: "/loop", when: "goal needs an explicit --for/--iters budget across multiple autonomous cycles" }, + { 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" }, ]; } if (platform === "cursor") { return [{ cmd: "@knitbrain rules", when: "re-read knitbrain usage rules" }]; } + 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" }, + ]; + } return []; } diff --git a/src/version.ts b/src/version.ts index cda658c..ed81662 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ /** Single source of truth for server identity. */ -export const VERSION = "0.19.0"; +export const VERSION = "0.20.0"; export const SERVER_NAME = "knitbrain"; diff --git a/tests/platforms.test.ts b/tests/platforms.test.ts index 924753f..d959de4 100644 --- a/tests/platforms.test.ts +++ b/tests/platforms.test.ts @@ -13,6 +13,8 @@ import { vscodeArtifacts, windsurfArtifacts, codexSnippet, + loopLaunchInstructions, + goalOrchestrationInstructions, universalArtifacts, slashCommands, GOAL_LOOP_NUDGE, @@ -190,6 +192,85 @@ describe("cross-platform hook config emitters (Tier-1.1 — merge, never clobber expect(cmds).toContain("/loop"); }); + // Cross-platform /loop: 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", () => { + const body = loopLaunchInstructions("claude -p"); + expect(body).toContain("nohup knitbrain loop"); + expect(body).toContain("BACKGROUND"); + expect(body).toContain('--agent "claude -p"'); + 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")!; + expect(t).toBeDefined(); + expect(t.content).toContain("description ="); + expect(t.content).toContain('prompt = """'); + expect(t.content).toContain("knitbrain loop"); + 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")!; + 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")!; + expect(w).toBeDefined(); + expect(w.content).toContain("name: loop"); + expect(w.content).toContain("## Steps"); + expect(w.content).toContain("knitbrain loop"); + }); + + it("Cursor rules document the terminal runner (no native slash command)", () => { + const rules = cursorArtifacts().find((a) => a.path.endsWith(".mdc"))!; + expect(rules.content).toContain("knitbrain loop goal.md"); + }); + + it("Codex snippet documents the global /loop prompt (codex exec worker)", () => { + const snip = codexSnippet(cfg); + expect(snip).toContain("~/.codex/prompts/loop.md"); + expect(snip).toContain("codex exec"); + }); + + it("slashCommands includes /loop on gemini, vscode, windsurf, codex", () => { + for (const p of ["gemini", "vscode", "windsurf", "codex"]) { + expect(slashCommands(p).map((c) => c.cmd)).toContain("/loop"); + } + }); + + // /goal parity: in-session orchestration front door, on every platform /loop + // reaches (no gaps). Distinct from /loop (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", () => { + 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 + }); + + 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("slashCommands lists /goal AND /loop 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"); + } + }); + it("KNITBRAIN_HOOKS wires SubagentStart + SubagentStop (ambient orchestration + attribution)", () => { expect(KNITBRAIN_HOOKS.SubagentStart[0]!.hooks[0]!.command).toBe("knitbrain-hook subagentstart"); expect(KNITBRAIN_HOOKS.SubagentStop[0]!.hooks[0]!.command).toBe("knitbrain-hook subagentstop");