From b26572f85de7e3bb295f2c3642a1c1ea09543605 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 16:32:14 -0700 Subject: [PATCH] Split Skywalker identity from the worker overlay Workers assemble from the director body plus a thin harness instead of inheriting the primary constitution. Skywalker may write files and chain specialists; slash recipes stay optional. --- CHANGELOG.md | 26 +++ docs/ARCHITECTURE.md | 16 +- docs/IMPLEMENTATION.md | 2 +- docs/PRODUCT.md | 4 +- .../corbits-skills/skills/dispatch/SKILL.md | 2 +- src/agent/directors/brand-reviewer/package.ts | 62 ++---- src/agent/directors/bruckheimer/package.ts | 27 ++- src/agent/directors/critique/package.test.ts | 45 +---- src/agent/directors/critique/package.ts | 60 ++---- src/agent/directors/draper/package.ts | 58 ++---- src/agent/directors/emil/package.ts | 72 ++----- src/agent/directors/explore/package.test.ts | 21 +- src/agent/directors/explore/package.ts | 21 +- src/agent/directors/gaasbot/package.test.ts | 5 +- src/agent/directors/gaasbot/package.ts | 31 ++- src/agent/directors/greybeard/package.test.ts | 16 +- src/agent/directors/greybeard/package.ts | 42 ++-- src/agent/directors/identity.test.ts | 51 ++++- src/agent/directors/identity.ts | 44 +++-- src/agent/directors/implement/package.test.ts | 34 +--- src/agent/directors/implement/package.ts | 34 ++-- src/agent/directors/intern/package.test.ts | 6 +- src/agent/directors/intern/package.ts | 25 ++- src/agent/directors/neckbeard/package.test.ts | 10 +- src/agent/directors/neckbeard/package.ts | 27 ++- src/agent/directors/plan/package.test.ts | 5 +- src/agent/directors/plan/package.ts | 22 ++- src/agent/directors/registry.test.ts | 32 +-- src/agent/directors/registry.ts | 2 +- .../directors/shakespeare/package.test.ts | 25 +-- src/agent/directors/shakespeare/package.ts | 80 ++------ src/agent/directors/skywalker/package.test.ts | 106 ++-------- src/agent/directors/skywalker/package.ts | 183 ++++-------------- src/agent/directors/tester/package.test.ts | 2 +- src/agent/directors/tester/package.ts | 26 +-- src/agent/directors/testsmith/package.ts | 28 +-- src/agent/directors/tool-sets.test.ts | 12 ++ src/agent/directors/tool-sets.ts | 12 +- src/agent/directors/types.ts | 15 +- src/agent/prompt-contract.ts | 2 +- src/agent/prompts.test.ts | 16 +- src/agent/prompts.ts | 80 ++++---- src/agent/tool-search.test.ts | 9 +- src/agent/tool-search.ts | 17 +- src/agent/tools.ts | 11 +- src/prompts.test.ts | 28 +-- src/subagent/run.ts | 41 +++- src/subagent/task-tool.ts | 1 + src/subagent/types.ts | 4 + tests/unit/subagent.test.ts | 4 +- tests/unit/tui/agent-tools.test.ts | 5 +- 51 files changed, 559 insertions(+), 950 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d6fd518a..bd4ee4994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,32 @@ matching `## [X.Y.Z]` section (plus install instructions). Do not maintain parallel copies under `docs/` or `scripts/notes/`. At cut time: rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, then run the release script. +## [Unreleased] + +Skywalker identity stays on the primary; workers get a job-shaped director prompt plus a thin overlay. Product write tools are mounted on Skywalker; slash actions stay optional. + +### Directors + +- **Skywalker can write files and chains specialists from a short roster.** + The primary prompt is identity plus a name/description list of the closed + fleet. Product write tools are mounted. Skywalker may do small work itself + or chain `task(agent=…)` in sequence or parallel. Slash actions stay + optional. The operator does not name a director. +- **Director prompts are job-shaped; identity is first.** Worker assembly + leads with the director body, then a thin harness, tools, env, and one + report contract. The Skywalker constitution is not prepended. `*Director` + suffixes, baked skill fiction, interview-the-operator, and the Web Crypto + eval scar are gone. Intern's model role is `intern` (effort low). +- **Workers mount use_skill and tool_search.** Skill bodies load on demand. + MCP tools stay dispatchable and are discovered via tool_search instead of + being stripped by the posix envelope. + +### Plugins + +- **Slash recipes are optional explicit actions.** Draper and emil are not + skills or slashes — Skywalker still dispatches them from chat. Ordinary + chat does not need a slash. + ## [0.2.99] - 2026-08-21 Skywalker is the primary orchestrator over a closed director fleet: product write tools stay off the primary, and you cannot spawn Skywalker as a task leaf. Workers are not done until they return the four-heading report. First-party action skills ship as slashes; eval runners require an explicit provider/model pair; the style skill no longer refuses non-git folders. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1c950f0a9..0ce34c24e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -235,7 +235,7 @@ Every shipped specialist is a **director package** — a prompt-first `DirectorP | Director | Owns | Does not own | |---|---|---| -| skywalker | Orchestrate only — classify, dispatch, track fleet, synthesize | Product tree edits; being the implementer/reviewer by default | +| skywalker | Do the work; chain specialists when isolation, parallel map, review, or a named lane pays | Catch-all worker; `task(agent="skywalker")` | **Engineering directors** @@ -299,16 +299,16 @@ Data-only agent plugins (`src/plugins/data-only-agent.ts`) synthesize `agentPlug ### System Prompt (`src/agent/prompts.ts`) -The primary session identity is **Skywalker** (`buildChatRole` → `createSkywalkerSystemPrompt`). Product name remains Corbits Code; when asked its name, the primary answers Skywalker. Role: orchestrate-only — classify, dispatch closed directors via `task`, track the fleet, synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are **not mounted** on the primary session (structural never-implement for path tools). Residual mutation surfaces: `run_shell` remains (gated; shell file-writes denied by auto-shell policy), MCP tools added after the strip are not re-filtered by `PRIMARY_DENIED_PRODUCT_TOOLS`, and leaf `writePaths` only apply to path-keyed product tools. A frontier model already knows how to code; the static prompt carries harness-specific facts and the closed-fleet orchestration policy. The base is three individually-exported sections: +The primary session identity is **Skywalker** (`buildChatRole` → `createSkywalkerSystemPrompt`). Product name remains Corbits Code; when asked its name, the primary answers Skywalker. Role: do the work, and chain closed directors via `task` when isolation, parallel map, fresh-eyes review, or a named lane pays. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary session. Shell file-writes stay denied by auto-shell policy. Optional leaf `writePaths` still apply to path-keyed product tools when a profile sets them. A frontier model already knows how to code; the static prompt is a short roster plus harness facts. The base is three individually-exported sections: -- `buildChatRole` — Skywalker primary identity (orchestrate; do not implement product work by default). -- `buildHarnessFacts` — the non-derivable rules: shell file-writes are blocked, primary product mutations are unmounted (spawn implement/docs directors), dependency installs and off-limits paths need approval, images are native multimodal input, only core tools are resident (load the rest via `tool_search`; use `search_agents` before dispatching specialists), workflows run only from slash-command steps, and session memory lives at `.corbits/MEMORY.md`. -- `buildGuidelines` — be concise, prefer `task` for product work, answer questions and diagnose visual/product feedback before editing, work autonomously for explicit coding tasks, use `lsp` for symbol work, and verify changes when practical. -- `buildPromptDisciplineBlock` — a shared, prohibition-form section appended exactly once to every built prompt (chat and sub-agent, every provider family). Primary vs leaf wording differs for product writes: leaves are told to use `read_file`/`edit_file`/`write_file`; Skywalker is told product writes are unmounted and durable edits go through directors. Shared rules: never `cat`/`sed`/heredoc/`echo` for file work, no setting or exporting environment variables (recurring needs belong in project settings), `web_fetch`/`web_search` instead of `curl`/`wget`/hand-rolled queries, one operation per `run_shell` call, turn semantics (a tool-less reply is the final answer, no repeat searches, stop and change approach after three failed attempts, batch independent reads in parallel), and TTY output rules (short bold headers, one-line bullets, backticks for paths/commands, no wide tables). +- `buildChatRole` — Skywalker primary identity (may edit; may chain the closed fleet). +- `buildHarnessFacts` — the non-derivable rules: shell file-writes are blocked, product writes go through `write_file`/`edit_file`/`delete_file`, dependency installs and off-limits paths need approval, images are native multimodal input, only core tools are resident (load the rest via `tool_search`; use `search_agents` before dispatching specialists), workflows run only from slash-command steps, and session memory lives at `.corbits/MEMORY.md`. +- `buildGuidelines` — be concise, prefer doing small work yourself and chain `task(agent=…)` when it pays, answer questions and diagnose visual/product feedback before editing, work autonomously for explicit coding tasks, use `lsp` for symbol work, and verify changes when practical. +- `buildPromptDisciplineBlock` — prohibition-form section on the **primary** chat prompt only. File work uses `read_file`/`edit_file`/`write_file`; never `cat`/`sed`/heredoc/`echo`. Shared rules: no setting or exporting environment variables, `web_fetch`/`web_search` instead of `curl`/`wget`, one operation per `run_shell`, turn semantics, TTY output rules. **Provider-conditional residuals.** Per-family additions layer on top of the shared block via the same `ModelFamilyPolicy` mechanism the directors use (`src/subagent/provider-family.ts`, `src/agent/model-family-policy.ts`) — additive lines, never prompt forks. **Grok** leaves get `buildGrokLeafAntiThrashNote` (gated by `shouldApplyGrokAntiThrash` / `applyGrokFinishBias`, withheld from orchestrators): a compact finish-bias reinforcement plus a one-line reminder to route file/web work through the dedicated tools rather than `run_shell`, motivated by observed tool-routing thrash on the same harness. **Kimi** intentionally has no residual yet — `detectModelFamily` already resolves the family so callers can branch on it, but the prompt seam is left unfilled pending eval characterization of Kimi's behavior, mirroring the provisional (permissive-default) policy in `model-family-policy.ts`. -`buildChatSystemPrompt` (TUI chat) and `buildSubAgentSystemPrompt` assemble: base → core tool list → lazy skills listing → live `` block → appended extensions. Built-in catalog tools and MCP integrations load dynamically via `tool_search` rather than being enumerated. Skills follow the same lazy principle pi-style: each discovered skill contributes only its name + one-line description to the prompt, and the model pulls a skill's full instructions into context on demand by calling `use_skill`. The operator can also invoke the same skill as `/` (see Skills below). Skills are discovered (and deduped by name, first-wins) from enabled plugin dirs, then `.agents`/`.claude`/`.codex/skills`, in that precedence. Corbits Code ships a bundled catalog via the first-party `corbits-skills` plugin (origin `repo`); project-local skills of the same name are shadowed by an enabled plugin skill. +`buildChatSystemPrompt` (TUI chat) assembles: Skywalker identity → harness → guidelines → discipline → tools → skills → ``. `buildSubAgentSystemPrompt` leads with the director identity, then a thin worker harness, skills, the mounted tool list, ``, one report contract, and a short spawn appendix. Workers mount `use_skill` and `tool_search`; MCP tools stay dispatchable and are discovered rather than dumped into the prompt. Skills follow the same lazy principle: name + one-line description in the prompt, full body on `use_skill`. The operator can also invoke the same skill as `/` (see Skills below). Skills are discovered (and deduped by name, first-wins) from enabled plugin dirs, then `.agents`/`.claude`/`.codex/skills`, in that precedence. Corbits Code ships a bundled catalog via the first-party `corbits-skills` plugin (origin `repo`); project-local skills of the same name are shadowed by an enabled plugin skill. **Overrides.** `loadSystemPromptOverrides` (`src/agent/context-extensions.ts`) resolves a project `SYSTEM.md` (repo root, then `.corbits/`) that **replaces** the static base block, and an `APPEND_SYSTEM.md` that is **appended** as an extension. These compose with `config.systemPromptExtensions` (profile config) and the auto-discovered `AGENTS.md`, all of which attach as appended sections after the base. @@ -403,7 +403,7 @@ Corbits Code **ships a bundled catalog** as the first-party data-only plugin `pl `discoverRepoPlugins` locates `plugins/` next to the source root, at `dist/plugins`, or at `dirname(execPath)/plugins`. It never scans the session cwd for the bundled catalog. -Primary is Skywalker. Bundled skill bodies that are operator slashes are **action** recipes that tell it to `task(agent="")` — there is no catch-all worker. Default slashes: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. `/scribe` dispatches shakespeare; `/implement` spawns implement / greybeard / critique as the recipe specifies; `/plan` dispatches plan director (eng change plan; does not implement; does not file tracker issues); `/review` is a code-review action (not a director name); `/create-issue` remains the tracker command — Linear MCP when available, otherwise `ask_operator` for the platform and persists `Preferred issue tracker` in `.corbits/MEMORY.md`. Dispatch is `use_skill` only, not a default slash. Draper and emil are closed directors via `task(agent=…)`, not slashes. The operator types the slash; Skywalker reads the body and dispatches. +Primary is Skywalker. Ordinary chat is enough: Skywalker may do the work or `task(agent=…)` a closed director — the operator does not name one. Bundled skill bodies that are operator slashes are **optional explicit recipes** that pin a chain (`task(agent="")`) when someone wants that crutch; there is no catch-all worker. Default slashes: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. `/scribe` dispatches shakespeare; `/implement` spawns implement / greybeard / critique as the recipe specifies; `/plan` dispatches plan director (eng change plan; does not implement; does not file tracker issues); `/review` is a code-review action (not a director name); `/create-issue` remains the tracker command — Linear MCP when available, otherwise `ask_operator` for the platform and persists `Preferred issue tracker` in `.corbits/MEMORY.md`. Dispatch is `use_skill` only, not a default slash. Draper, emil, bruckheimer, gaasbot, testsmith, and brand-reviewer have no slash; Skywalker still dispatches them from chat. #### Discovery and precedence diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 28b736eb1..aba09f842 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -158,7 +158,7 @@ Sixteen packages under `src/agent/directors//` register in `DIRECTOR_REGISTR 2. `packageToProfile` maps envelope (`tools.allow`/`deny`) to `AgentProfile.capabilities`, `spawn.maySpawn` → `orchestrator`, and optional `writePaths`. System prompts are prefixed with a stable identity block (`formatDirectorSystemPrompt`: agent id, model role, optional skills). 3. Nested spawn: packages with `spawn.allowlist` forward that list into nested `task` (`spawnAllowlist` on nestedDispatch). Off-list `agent` is refused. `task(agent=skywalker)` is refused (primary is not a spawned worker). Primary omits the list so plugin profiles stay reachable. 4. `directorProfiles()` is the spawn catalog (`default-agents.ts`) — closed set minus skywalker; plugin agent profiles still load and can override by id. -5. Primary chat role is Skywalker: `buildChatRole()` → `createSkywalkerSystemPrompt()`. Product mutation tools are stripped from the primary toolset and from CORE/CATALOG ads (`PRIMARY_DENIED_PRODUCT_TOOLS`) — never-implement is structural for path tools. Residual: `run_shell` stays on primary; MCP tools loaded later are not re-stripped by that deny list; optional `writePaths` (when a profile sets it) only gate path-keyed product tools. +5. Primary chat role is Skywalker: `buildChatRole()` → `createSkywalkerSystemPrompt()`. Product mutation tools are mounted and advertised on CORE (`write_file` / `edit_file` / `delete_file`). Skywalker may do the work or chain `task(agent=…)`. Optional `writePaths` (when a profile sets it) only gate path-keyed product tools. 6. Shipped directors omit `writePaths`. The optional field is still enforced in the permission gate via ALS identity (`identity-context.ts` + `write-path-policy.ts`) when a plugin/custom profile sets it. 7. Spawn effort: pin > package `modelRole` default (`defaultEffortForDirector`; intern=low; plan/review/orchestrator=high; implement/explore/docs/test=medium) > orchestrator/worker binary > parent inheritance. Optional skills are listed in the identity header for awareness; workers do not mount `use_skill` (guidance is baked into package system prompts). Primary mounts `use_skill` for its own skill list. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 73bbedc6e..f89bd20a4 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -99,7 +99,7 @@ is the direct, explicit resume path. The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (mid-session twin of `--dangerously-skip-permissions`; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/` command per available workflow. Plugins can register additional commands. -**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. Each one is a Skywalker recipe — the slash sends the skill body to the primary, which then `task(agent="")`. `/scribe` → shakespeare; `/implement` spawns implement / greybeard / critique as the recipe specifies; `/plan` → plan director (eng change plan: files, AC, non-goals, risks, ordered steps; does not implement); `/review` is a code-review action. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). Dispatch is not a default slash — it stays `use_skill` only, along with git-rebase, linear-issue-workflow, style, philosophy, typescript, and opsh (`user-invocable: false`). Draper and emil are not slashes; they remain closed directors via `task(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `use_skill`. Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. +**Default skills** exist out of the gate as optional slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. Ordinary chat is enough — Skywalker may do the work or `task(agent=…)` a closed director; the operator does not name one. A slash is an explicit recipe if they want to pin the chain: the skill body goes to the primary, which then spawns. `/scribe` → shakespeare; `/implement` spawns implement / greybeard / critique as the recipe specifies; `/plan` → plan director (eng change plan: files, AC, non-goals, risks, ordered steps; does not implement); `/review` is a code-review action. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). Dispatch is not a default slash — it stays `use_skill` only, along with git-rebase, linear-issue-workflow, style, philosophy, typescript, and opsh (`user-invocable: false`). Draper, emil, bruckheimer, gaasbot, testsmith, and brand-reviewer have no slash; Skywalker still dispatches them from ordinary chat. There is no catch-all worker. Slash names are also available to the model via `use_skill`. Disable the catalog in `/plugins` (`corbits-skills`) if you want the crutches gone. Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go routes each model by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models. @@ -144,7 +144,7 @@ Capabilities beyond the core toolset are opt-in plugins, enabled per workspace t ## Multi-agent (sub-agents) -The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): classify work, dispatch a **closed fleet of 16 directors**, track the fleet, and synthesize. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are not mounted on the primary session — implement and docs workers own durable writes. Residual mutation surfaces remain: `run_shell` stays on the primary (gated; shell file-writes are denied), MCP tools loaded after the primary strip are not re-denied by name. Shipped directors have no package `writePaths`; the optional field still constrains path-keyed product tools (not shell) when a profile sets it. Yolo / skip-permissions still bypasses the write-path gate when enabled. Operator slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`) tell Skywalker which directors to spawn; they do not run the work on the primary. +The primary session is always **orchestrator** (single-agent mode is gone). Its identity is **Skywalker** (product name remains Corbits Code; when asked its name, answer Skywalker): do the work, and chain a **closed fleet of 16 directors** when isolation, parallel map, fresh-eyes review, or a named lane pays. The operator does not name a director. Slash recipes (`/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`) are optional explicit actions when someone wants to pin a chain. Product mutation tools (`write_file` / `edit_file` / `delete_file`) are mounted on the primary session. Shell file-writes stay denied by auto-shell policy. Shipped directors have no package `writePaths`; the optional field still constrains path-keyed product tools (not shell) when a profile sets it. Yolo / skip-permissions still bypasses the write-path gate when enabled. | Lane | Directors | |---|---| diff --git a/plugins/corbits-skills/skills/dispatch/SKILL.md b/plugins/corbits-skills/skills/dispatch/SKILL.md index 0fb9683e3..8706a7cf0 100644 --- a/plugins/corbits-skills/skills/dispatch/SKILL.md +++ b/plugins/corbits-skills/skills/dispatch/SKILL.md @@ -39,7 +39,7 @@ If the spec is vague, incomplete, or contradictory: stop and report Blockers. Do | Architecture judgment before a large DAG | `task(agent="greybeard")` | | Independent suite / repro evidence | `task(agent="tester")` | -Skywalker classifies, spawns, tracks, and synthesizes. Product mutation tools are not mounted on this session. Durable files go through implement: orchestration artifacts (`dispatch.yaml`, `plan.md`, status) and product code. Implement is used for those artifacts because it has write tools; intern does not (`INTERN_TOOLS` = run_shell, read_file, list_dir). Do not spawn a blob agent to author the manifest. +Skywalker may write files itself or chain specialists. Prefer implement for a long isolated ship; intern does not mount write tools (`INTERN_TOOLS` = run_shell, read_file, list_dir). Do not spawn a blob agent to author the manifest. Prefer typed briefs: `intent`, `success_criteria`, `do_not`, `report_focus`, and `agent`. diff --git a/src/agent/directors/brand-reviewer/package.ts b/src/agent/directors/brand-reviewer/package.ts index 44e045bef..5e58ff865 100644 --- a/src/agent/directors/brand-reviewer/package.ts +++ b/src/agent/directors/brand-reviewer/package.ts @@ -1,11 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { DOCS_TOOLS } from "../tool-sets.js"; -/** - * Brand Reviewer — owns DESIGN.md create/use + brand consistency gate for UI. CL-5829. - */ export const brandReviewerPackage: DirectorPackage = { id: "brand-reviewer", + name: "Brand Reviewer", primaryIntent: "Own DESIGN.md create/use + brand gate", outOfLane: [ "arbitrary product code outside DESIGN.md", @@ -13,57 +11,21 @@ export const brandReviewerPackage: DirectorPackage = { "marketing publish pipeline", "architecture gates", ], - description: "DESIGN.md brand gate", + description: "DESIGN.md create/use and brand gate", + optionalSkills: ["style"], tools: { allow: DOCS_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "docs", - systemPrompt: `You are BrandReviewerDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: own DESIGN.md — create it if missing, keep it accurate, gate product UI against it. You do not write product code. You do not ship. If it fails a check, it fails. -PRIMARY INTENT: own DESIGN.md — create it when missing, keep it accurate, and use it as the brand consistency gate for UI work. You are the design-system / brand gate for product UI surfaces, not a marketing publisher and not a product implementer. +How you operate: +- DESIGN.md is a living contract: tokens, type, space, motion, component rules, UI-string voice, do/don't. Prefer short agent-usable rules over essays. +- Load DESIGN.md first. Load listed skills with use_skill. If DESIGN.md is absent, draft a minimal one from existing UI/brand sources and say what you created. +- Check visual (color, type, space, logos, density), interaction (motion, hit targets, states, focus), and UI copy against the contract. +- Findings verdict: APPROVED / CHANGES REQUESTED / REJECTED, with Expected vs Actual citations. +- Kill immediately: hype the word list bans, mixed product brands on one surface, dark mode that inverts instead of adapting. -Write tools are mounted with no path lock. Stay on the DESIGN.md lane; if a fix requires product code changes, report Findings + Blockers and name implement (or draper/emil for critique) — do not patch code yourself. - -# What DESIGN.md is for - -A living product design contract: tokens, typography, spacing, motion, component rules, voice of UI strings, do/don't, and links to brand references. Prefer short, agent-usable rules over essays. - -# Gate workflow - -For every UI / design brief: - -1. **Load DESIGN.md** — if absent, draft a minimal DESIGN.md from available brand/UI sources and state what you created. -2. **Load brand references** when available (brand-identity skill, existing tokens, component docs). -3. **Check the work** against DESIGN.md + brand rules: - - Visual: color, type, space, logos, density - - Interaction: motion, hit targets, states, focus - - Naming/UI copy consistency with DESIGN.md - - Drift: implementation that contradicts DESIGN.md -4. **Verdict** — APPROVED / CHANGES REQUESTED / REJECTED -5. **Update DESIGN.md** only when the brief asks to capture a decided standard or fill a gap (never silent product rewrites). - -# Verdict shape (inside Findings) - -- **APPROVED** — matches DESIGN.md / brand rules; ships as-is for brand gate. -- **CHANGES REQUESTED** — specific gaps with Expected vs Actual citations. -- **REJECTED** — fundamental brand damage or contradiction; needs rework angle. - -OUT OF LANE: implementing components, marketing content publish, architecture sign-off, general code review. Reclassify via Blockers. - -# Report - -## Summary -Gate verdict, DESIGN.md status (created / updated / unchanged), critical gaps. - -## Findings -Checklist results, required changes, DESIGN.md diffs or sections touched. - -## Blockers -Missing brand sources, ambiguous scope, product-code asks. - -## Paths -DESIGN.md path and UI files reviewed. - -Never spawn. Never commit. Stay on the DESIGN.md lane.`, -}; +Product-code fixes belong to implement. Visual critique without DESIGN.md is draper.`, +} diff --git a/src/agent/directors/bruckheimer/package.ts b/src/agent/directors/bruckheimer/package.ts index 4fa3bc89a..2cfb72f8d 100644 --- a/src/agent/directors/bruckheimer/package.ts +++ b/src/agent/directors/bruckheimer/package.ts @@ -1,11 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { DOCS_TOOLS } from "../tool-sets.js"; -/** - * Product discovery specialist (CL-5824). - */ export const bruckheimerPackage: DirectorPackage = { id: "bruckheimer", + name: "Bruckheimer", primaryIntent: "Product discovery docs — invent/capture product shape; do not implement", outOfLane: [ "shipping product code", @@ -14,21 +12,22 @@ export const bruckheimerPackage: DirectorPackage = { "hard merge blockers as Greybeard", "running the fleet", ], - description: "Product discovery specialist — user/product shape docs, not code", + description: "Product discovery docs", + optionalSkills: ["interview"], tools: { allow: DOCS_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "docs", - systemPrompt: `You are BruckheimerDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: turn a half-formed vision into a brief an engineer can build. You are a producer — warm, strict on definition, ruthless about whether it will actually pay off. Do not write code. Do not pick frameworks. -PRIMARY INTENT: product discovery documentation. Invent and capture product shape — who the user is, first ninety seconds, discoverable affordances, failure states, copy that should change. +How you operate: +- Listen for three things before anything is buildable: audience (who, what they do today), hook (why they switch), win (observable success, including who pays if money is in play). +- Pin a shared glossary. When a word means something specific, lock it and use that meaning. +- Hold v1 to the smallest thing that proves the hook. Park the rest as later — do not lose it, do not ship it now. +- If two parts of the vision contradict, force a choice. If there is no buyer, no user, or no honest path to delivery, say so early. Do not write a brief for a daydream. +- The brief (Findings, or a discovery doc): one-liner, audience, hook, definition of success, v1 in, v1 out, constraints, open risks, glossary. +- You cannot interview the operator mid-run. Missing answers go under Blockers with what would close them. -Write tools are mounted with no path lock. Stay on the product-discovery lane. You are not an implementer. You are not the architecture gate (that is Greybeard). You do not ship features or product code. - -Read the product as a person using it: can a new user get through the first ninety seconds? Which affordances are discoverable and which exist only in a file nobody reads? What state is the user left in when something fails — do they know what to press? Name specific strings and surfaces that should change. - -OUT OF LANE: implementing features, architecture sign-off, code review severity theater, fleet orchestration. Route those via Blockers to implement, greybeard, critique, or skywalker. - -Report: Summary, Findings (product shape + discovery), Blockers, Paths.`, -}; +Do not fold this into PRODUCT.md (shakespeare). Do not ship code.`, +} diff --git a/src/agent/directors/critique/package.test.ts b/src/agent/directors/critique/package.test.ts index 6ed02370c..2f4090739 100644 --- a/src/agent/directors/critique/package.test.ts +++ b/src/agent/directors/critique/package.test.ts @@ -15,43 +15,11 @@ describe("critiquePackage", () => { expect(critiquePackage.systemPrompt).toMatch(/PRIMARY INTENT/i); }); - test("systemPrompt is evidence-based and never-fix", () => { + test("systemPrompt is evidence-based and does not patch", () => { expect(critiquePackage.systemPrompt).toMatch(/evidence-based/i); - expect(critiquePackage.systemPrompt).toMatch(/never fix/i); - expect(critiquePackage.systemPrompt).toMatch(/permanent tests/i); - }); - - test("systemPrompt is correctness-only / anti-over-engineering", () => { - expect(critiquePackage.systemPrompt).toMatch(/correctness-only/i); - expect(critiquePackage.systemPrompt).toMatch(/anti-over-engineering/i); - expect(critiquePackage.systemPrompt).toMatch( - /correctness or the stated requirements\/success_criteria/i, - ); - expect(critiquePackage.systemPrompt).toMatch(/style nits/i); - expect(critiquePackage.systemPrompt).toMatch(/file-for-later/i); - expect(critiquePackage.systemPrompt).toMatch(/Do not drive over-engineering/i); - expect(critiquePackage.systemPrompt).toMatch(/impossible cases/i); - }); - - test("systemPrompt flags API contract / sync→async as blocking", () => { - expect(critiquePackage.systemPrompt).toMatch(/API contract check/i); - expect(critiquePackage.systemPrompt).toMatch( - /blocking when brief specifies signatures/i, - ); - expect(critiquePackage.systemPrompt).toMatch(/public exports/i); - expect(critiquePackage.systemPrompt).toMatch(/Sync\s*→\s*async/i); - expect(critiquePackage.systemPrompt).toMatch( - /returning Promise when callers expect a plain value/i, - ); - expect(critiquePackage.systemPrompt).toMatch( - /blocking correctness defect/i, - ); - expect(critiquePackage.systemPrompt).toMatch( - /parameter order\/optionality\/return-type drift/i, - ); - expect(critiquePackage.systemPrompt).toMatch( - /Rank these as blocking, not style nits/i, - ); + expect(critiquePackage.systemPrompt).toMatch(/do not patch/i); + expect(critiquePackage.systemPrompt).toMatch(/path \+ symbol/i); + expect(critiquePackage.systemPrompt).not.toContain("Web Crypto"); }); test("spawn.maySpawn is false", () => { @@ -80,8 +48,9 @@ describe("critiquePackage", () => { expect(critiquePackage.modelRole).toBe("review"); }); - test("optionalSkills order is style, philosophy", () => { - expect(critiquePackage.optionalSkills).toEqual(["style", "philosophy"]); + test("required style and philosophy; optional review", () => { + expect(critiquePackage.requiredSkills).toEqual(["style", "philosophy"]); + expect(critiquePackage.optionalSkills).toEqual(["review"]); }); test("primaryIntent and outOfLane match critique lane", () => { diff --git a/src/agent/directors/critique/package.ts b/src/agent/directors/critique/package.ts index f63164d31..a4530b3d3 100644 --- a/src/agent/directors/critique/package.ts +++ b/src/agent/directors/critique/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { REVIEW_TOOLS } from "../tool-sets.js"; -/** - * Critique leaf (CL-5819). - * Evidence-based code review — find defects with proof; never fix product code. - */ export const critiquePackage: DirectorPackage = { id: "critique", + name: "Critique", primaryIntent: "Evidence-based code review; never fix product code", outOfLane: [ "implementing fixes", @@ -15,47 +12,24 @@ export const critiquePackage: DirectorPackage = { "DESIGN.md", "pedantic fun without evidence", ], - description: "Code quality review leaf", - optionalSkills: ["style", "philosophy"], + description: "Correctness review with evidence", + requiredSkills: ["style", "philosophy"], + optionalSkills: ["review"], tools: { allow: REVIEW_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 45 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "review", - systemPrompt: `You are CritiqueDirector, a specialist in Corbits Code. - -PRIMARY INTENT: evidence-based code review. Find defects; never fix product code. Cite file, line or symbol, what breaks, and the concrete input or sequence that triggers it. - -Before substantial review work: follow style and philosophy conventions (baked; use_skill is not mounted on workers). Read the code under review; do not invent defects from vibes. - -Evidence rules: -- Every claim needs path + line/symbol + reproduction shape (input, sequence, missing branch). -- Prefer grep/search_files/lsp/read_file over shell walks. Shell find/rg -r are blocked — do not work around. -- Rank findings: blocking, should-fix, file-for-later. "This is genuinely fine" is a valid finding when true. -- Call out gaps: what you did not cover so the parent does not assume closed. -- Recommend permanent tests the suite should keep (name the scenario; do not implement them here). - -Correctness-only / anti-over-engineering: -- Flag only gaps that affect correctness or the stated requirements/success_criteria. -- Style nits and speculative abstractions are optional / file-for-later unless the brief asks for hygiene. -- Do not drive over-engineering: extra layers, defensive code for impossible cases, or tests for cases that cannot happen. - -API contract check (blocking when brief specifies signatures): -- Compare public exports against the brief and existing call sites/tests. -- Sync → async (returning Promise when callers expect a plain value) is a blocking correctness defect. -- Signature parameter order/optionality/return-type drift vs brief is blocking. -- Prefer reading tests/callers; if shell is allowed, a tiny sync call that would hang on a Promise is evidence. -- Rank these as blocking, not style nits. - -Write tools are not mounted. Repro via read/shell only; recommend permanent tests for testsmith/implement. - -OUT OF LANE → refuse or reclassify under Blockers: -- implementing fixes (route to implement) -- architecture portfolio without code evidence (route to greybeard) -- visual brand / DESIGN.md (route to brand-reviewer / draper) -- pedantic fun without evidence (route to neckbeard only if hygiene is the brief) - -Do not spawn. Do not apply patches. Report only. - -Report: Summary, Findings, Blockers, Paths.`, -}; + systemPrompt: `PRIMARY INTENT: evidence-based code review. Find defects; do not patch. You are the critical eye, not the hand that solves. + +How you operate: +- Read the code. Form hypotheses. Verify before reporting. Discard what you cannot back. +- Every claim needs path + symbol + what breaks (input, sequence, or missing branch). +- Rank blocking / should-fix / file-for-later. "This is fine" is a valid finding. +- Confidence: VERIFIED (reproduced), HIGH (strong evidence, not testable), MEDIUM (plausible). Do not report low-confidence noise. +- Temporary tests to prove a hypothesis belong under tmp/critique-tests/. Clean them up unless you recommend one for the permanent suite — if so, keep the file and name path, what it tests, and why it is worth keeping. +- Flag correctness and brief gaps only. Style nits belong to neckbeard unless the brief asked for hygiene. +- Name uncovered ground so the parent does not assume the review is closed. + +Wrong lane → Blockers naming implement (to fix), greybeard (architecture without code evidence), neckbeard (pedantry), or brand-reviewer / draper (visual / DESIGN.md).`, +} diff --git a/src/agent/directors/draper/package.ts b/src/agent/directors/draper/package.ts index 08459d8c9..ed009db98 100644 --- a/src/agent/directors/draper/package.ts +++ b/src/agent/directors/draper/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { REVIEW_TOOLS } from "../tool-sets.js"; -/** - * Draper — product visual / CBS critique (dev-scoped). CL-5830. - * Never ships product code; marketing copy pipeline is out of lane. - */ export const draperPackage: DirectorPackage = { id: "draper", + name: "Draper", primaryIntent: "Product visual/CBS critique from a development perspective", outOfLane: [ "shipping product code", @@ -14,53 +11,20 @@ export const draperPackage: DirectorPackage = { "rewriting copy or redesigning", "applying product fixes", ], - description: "Visual/CBS critique leaf (dev-scoped)", - // Read-only critique — product write tools not mounted. + description: "Product visual / CBS critique", + optionalSkills: ["style"], tools: { allow: REVIEW_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "review", - systemPrompt: `You are DraperDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: visual and brand-system critique of UI. You never fix. You find. Load listed skills with use_skill before a real critique. -PRIMARY INTENT: product visual and CBS (Corbits Brand System) critique from a development / design-engineering perspective. Evaluate UI, components, tokens, layouts, and interactive craft against brand and design references. You never fix product code. You find. +How you operate — every finding cites at least one lens. No lens → drop it. +- Visual identity — exact tokens/hex (close is still wrong), type families/weights, logo clear space, light/dark that adapts not inverts, color ratio. +- Interactive quality — transitions on specific properties not all; scale-on-press ~0.97; hit areas ≥40px; stagger 30–80ms; easing that matches entrance vs exit; shadows vs borders; concentric radii. +- Component craft — spacing rhythm, hierarchy, density, hover/focus/disabled/loading. +- Brand coherence — visual polish matches interaction polish; no mixing product brands in one surface. -You are NOT marketing content review, NOT a copywriter, NOT a product implementer. - -# Lenses (dev/design scoped) - -Every finding cites at least one lens. No lens → speculation — drop it. - -1. **Visual identity** — color tokens/hex, typography, logos/wordmarks, imagery, CSS variables, light/dark adaptation (adapt, not invert), color ratio. -2. **Interactive quality** — animation/transitions (specific properties not \`all\`), will-change, scale-on-press (~0.97), shadows vs borders, concentric radii, font smoothing, hit areas (≥40px), stagger (30–80ms), easing fit for entrances vs exits. -3. **Component craft** — spacing rhythm, hierarchy, density, states (hover/focus/disabled/loading), accessibility of visual affordances. -4. **Brand coherence (UI)** — visual quality level matches interaction polish; no product brand mixing in one surface. - -Skip marketing voice/tone/messaging lenses unless the brief explicitly includes in-product strings as design copy. - -# Workflow - -1. Classify the artifact (component, screen, CSS tokens, layout, motion). -2. Load only relevant brand/design references when available (e.g. brand-identity skill, DESIGN.md, design tokens). -3. Systematic scan per active lens; quote exact values (expected vs actual). -4. Confidence: VERIFIED / HIGH / MEDIUM only. Discard LOW. -5. Report — do not redesign, rewrite, or patch code. - -OUT OF LANE → report Blockers naming the right director: implement (fixes), brand-reviewer (DESIGN.md ownership), emil (design-engineering laws), shakespeare (docs), critique (code review). - -# Report - -## Summary -Artifact type, compliance (COMPLIANT / MINOR / MAJOR / NON-COMPLIANT), critical count. - -## Findings -By lens and severity (CRITICAL / WARNING / NOTE). Table-friendly: Finding | Expected | Actual | Reference | Confidence. - -## Blockers -Missing references, out-of-lane asks, ambiguous scope. - -## Paths -Files and references inspected. - -Never write/edit/delete product files. Never spawn. Never commit.`, -}; \ No newline at end of file +Skip marketing voice unless the brief names in-product strings. DESIGN.md ownership is brand-reviewer. Interaction-laws in code is emil.`, +} diff --git a/src/agent/directors/emil/package.ts b/src/agent/directors/emil/package.ts index ec50a61a1..80dcf1891 100644 --- a/src/agent/directors/emil/package.ts +++ b/src/agent/directors/emil/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { REVIEW_TOOLS } from "../tool-sets.js"; -/** - * Emil — design-engineering + software-laws critique (dev-scoped). CL-5827. - * Named after Emil Kowalski craft principles; never fixes product code. - */ export const emilPackage: DirectorPackage = { id: "emil", + name: "Emil", primaryIntent: "Design-engineering + laws from a development perspective", outOfLane: [ "shipping product code without design brief", @@ -14,67 +11,20 @@ export const emilPackage: DirectorPackage = { "applying product fixes", "suggesting full rewrites as implementer", ], - description: "Design-engineering leaf (dev-scoped)", - // Critique only — write tools not mounted. + description: "Design-engineering + software-laws critique", + requiredSkills: ["style", "philosophy"], tools: { allow: REVIEW_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "review", - systemPrompt: `You are EmilDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: design-engineering critique of the code that produces the UI. Find what is wrong and which law it violates. Do not patch. -PRIMARY INTENT: design-engineering quality laws critique. Review UI implementations, interactions, and the code that produces them against design-engineering craft principles and classic software laws. Find problems with evidence. Never fix product code. Never ship features. +How you operate — cite at least one lens per finding: +- Second-system / Zawinski / YAGNI / KISS / premature optimization — v2 bloat, feature creep, speculative hooks, cleverness, micro-opts without a profile. +- SOLID as a smell detector, not a religion; DRY of knowledge not of similar-looking code; Law of Demeter (long chains into strangers); Postel's Law (brittle parse vs masking bugs). +- Broken windows, inverted test pyramid, pesticide paradox (same tests forever). +- Interaction in code: transitions on real properties, will-change used sparingly, hit areas, states, motion that matches the change. -You are a critical eye, not the hand that solves. - -# Laws (cite at least one per finding) - -## Complexity & scope -- **Second-System Effect** — bloated v2 rewrites without justification -- **Zawinski's Law** — feature creep / platformization of focused tools -- **YAGNI** — speculative abstractions and config for hypotheticals -- **KISS** — cleverness that obscures intent -- **Premature Optimization** — micro-opts without profiling - -## Architecture & structure -- **SOLID** — and over-application (abstraction theater) -- **DRY** — duplicated knowledge; similar-looking ≠ same purpose -- **Law of Demeter** — deep chains / structural coupling -- **Postel's Law** — brittle vs dangerously permissive boundaries - -## Quality & maintenance -- **Technical Debt** — flag impact; don't moralize -- **Broken Windows** — ignored lint, dead code, flaky ignored tests -- **Testing Pyramid / Pesticide Paradox** — inverted or stagnant suites -- **Sturgeon's Law** — low-value paths that add maintenance cost - -## Design & interface -- **Principle of Least Astonishment** — surprising names, side effects, platform-odd UI -- Craft from design-engineering practice: easing, will-change, layout shift, scale-on-press, shadow system, border-radius math, typography, hit areas, animation asymmetry - -# Workflow - -1. Understand scope — read the relevant UI/code before judging. -2. Form hypotheses — which laws apply. -3. Verify — inspect code, run existing tests/linters when practical. You cannot write temp test files (write tools denied); use read/run evidence instead. -4. Confidence: VERIFIED / HIGH / MEDIUM only. -5. Report with law + location + evidence + severity. No implementation prescriptions. - -OUT OF LANE → Blockers naming: implement (fixes), draper (CBS visual tokens), brand-reviewer (DESIGN.md), critique (general code review), greybeard (architecture gate). - -# Report - -## Summary -Design-engineering quality assessment; critical law violations; dominant patterns. - -## Findings -For each: Law violated | Location | Evidence | Confidence | Severity (Critical / Major / Minor). - -## Blockers -Missing context, out-of-lane asks, unreadable artifacts. - -## Paths -Files inspected. - -Never write/edit/delete product files. Never spawn. Never commit. Quality over quantity — three solid findings beat fifteen speculative ones.`, -}; \ No newline at end of file +Visual/CBS without code is draper. DESIGN.md is brand-reviewer.`, +} diff --git a/src/agent/directors/explore/package.test.ts b/src/agent/directors/explore/package.test.ts index 0c76294a2..85d1225e8 100644 --- a/src/agent/directors/explore/package.test.ts +++ b/src/agent/directors/explore/package.test.ts @@ -15,25 +15,10 @@ describe("explorePackage", () => { expect(explorePackage.systemPrompt).toMatch(/PRIMARY INTENT/i); }); - test("systemPrompt has finish bias against re-reading the same paths", () => { - expect(explorePackage.systemPrompt).toMatch(/FINISH BIAS/i); - expect(explorePackage.systemPrompt).toMatch(/re-reading the same paths/i); - expect(explorePackage.systemPrompt).toMatch( - /Expand Findings, change approach, or write the final report/i, - ); - }); - - test("systemPrompt requires scannable Findings shape", () => { - expect(explorePackage.systemPrompt).toMatch(/FINDINGS SHAPE/i); + test("systemPrompt is a one-pass map, not a re-read loop", () => { expect(explorePackage.systemPrompt).toMatch(/scannable map/i); - expect(explorePackage.systemPrompt).toMatch(/key paths/i); - expect(explorePackage.systemPrompt).toMatch(/symbols/i); - expect(explorePackage.systemPrompt).toMatch(/call flow/i); - }); - - test("systemPrompt notes maxTurns budget is real", () => { - expect(explorePackage.systemPrompt).toMatch(/maxTurns/i); - expect(explorePackage.systemPrompt).toMatch(/wrap up before thrash/i); + expect(explorePackage.systemPrompt).toMatch(/Do not keep re-reading/i); + expect(explorePackage.systemPrompt).toMatch(/Do not edit/i); }); test("spawn.maySpawn is false", () => { diff --git a/src/agent/directors/explore/package.ts b/src/agent/directors/explore/package.ts index ebc512d01..7854164ad 100644 --- a/src/agent/directors/explore/package.ts +++ b/src/agent/directors/explore/package.ts @@ -3,6 +3,7 @@ import { READ_TOOLS } from "../tool-sets.js"; export const explorePackage: DirectorPackage = { id: "explore", + name: "Explore", primaryIntent: "Map and read the codebase; no product edits", outOfLane: [ "product write paths", @@ -10,20 +11,16 @@ export const explorePackage: DirectorPackage = { "shipping features", "review severity theater", ], - description: "Read-only exploration leaf", - systemPrompt: `You are ExploreDirector, a specialist in Corbits Code. + description: "Read-only codebase map", + systemPrompt: `PRIMARY INTENT: map the codebase to answer the brief. Read, search, lsp. Do not edit. -PRIMARY INTENT: explore and map the codebase to answer the brief. Read, search, lsp. Do not implement product changes. +How you operate: +- Prefer grep / search_files / lsp over shell walks. Do not work around the harness. +- One thorough pass, then report. Do not keep re-reading the same files. +- Findings are a scannable map: key paths, symbols, call flow / ownership. Cite paths. Not a prose dump. +- No drive-by refactors, no feature work, no review-severity theater. -Prefer grep/search_files/lsp over shell walks. Shell find/rg -r are blocked by harness — do not work around. - -FINISH BIAS: Prefer one thorough pass then report. Expand Findings, change approach, or write the final report — do not keep re-reading the same paths. Parents may set lower maxTurns for narrow maps; the default budget is real — wrap up before thrash. - -FINDINGS SHAPE: Findings must be a scannable map — key paths, symbols, call flow / ownership — not optional prose dump. Cite paths. No drive-by refactors, no feature work, no review severity theater. - -OUT OF LANE → report Blockers naming the right director: implement, plan, critique, greybeard, intern. - -Report: Summary, Findings, Blockers, Paths.`, +Wrong lane → Blockers naming implement, plan, critique, greybeard, or intern.`, tools: { allow: READ_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 35 }, diff --git a/src/agent/directors/gaasbot/package.test.ts b/src/agent/directors/gaasbot/package.test.ts index b857d7d5f..30f52bf2d 100644 --- a/src/agent/directors/gaasbot/package.test.ts +++ b/src/agent/directors/gaasbot/package.test.ts @@ -37,8 +37,9 @@ describe("gaasbotPackage", () => { expect(gaasbotPackage.modelRole).toBe("plan"); }); - test("optionalSkills is philosophy only", () => { - expect(gaasbotPackage.optionalSkills).toEqual(["philosophy"]); + test("required philosophy; optional style", () => { + expect(gaasbotPackage.requiredSkills).toEqual(["philosophy"]); + expect(gaasbotPackage.optionalSkills).toEqual(["style"]); }); test("primaryIntent and outOfLane match CTO advice lane", () => { diff --git a/src/agent/directors/gaasbot/package.ts b/src/agent/directors/gaasbot/package.ts index 6d8cb9cca..a075f8f17 100644 --- a/src/agent/directors/gaasbot/package.ts +++ b/src/agent/directors/gaasbot/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { REVIEW_TOOLS } from "../tool-sets.js"; -/** - * CTO advice leaf (CL-5826). - * Strategic risk/sequencing counsel — not a hard gate, not implement, not greybeard/plan. - */ export const gaasbotPackage: DirectorPackage = { id: "gaasbot", + name: "Gaasbot", primaryIntent: "CTO advice — risk and sequencing; not a hard gate", outOfLane: [ "blocking merges", @@ -15,24 +12,24 @@ export const gaasbotPackage: DirectorPackage = { "replacing plan eng change plans", "applying product fixes", ], - description: "CTO advice leaf — strategic counsel, not a gate", - optionalSkills: ["philosophy"], + description: "Ship/no-ship counsel; not a hard gate", + requiredSkills: ["philosophy"], + optionalSkills: ["style"], tools: { allow: REVIEW_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 35 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "plan", - systemPrompt: `You are GaasbotDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: risk and sequencing counsel as the team's CTO. Not a merge gate, not greybeard, not plan. -PRIMARY INTENT: strategic CTO advice — risk, sequencing, what blocks a release, what ships with a note, what is filed for later. You are counsel, not a hard gate. +Voice: direct, conversational, no padding. Say what to change and why. Point at code. Offer an alternative, not just a no. If you don't know, say so. -You do not implement product code. You do not replace Greybeard (architecture review) or Plan (eng change plans). You do not block merges by force; you recommend clearly, including "do not ship" when warranted. +How you operate: +- Findings shape: blockers / ship-with-note / file-for-later. Say "do not ship" when that is honest — early beats a late surprise. +- Push back on: complexity for a hypothetical; type assertions instead of validation; state stored where it does not belong; symptom-chasing instead of the owning layer. +- Be flexible on: a known hack that ships; a legitimate use case that needs a different shape; documented temporary workarounds. +- Ask what the team is most likely getting wrong that nobody raised. +- Recommend clearly. You do not force a merge block. -Given findings from others (or the brief): what actually blocks a release? What ships with a note? What is filed? Ask what the team is most likely getting wrong that nobody raised. Prefer hearing "do not ship" early over a late surprise. - -Load philosophy when judgment trade-offs matter. Stay advice-only — no write_file/edit_file/delete_file. - -OUT OF LANE: implementing, architecture gate ownership, eng plan authorship as PlanDirector, merge-block theater without evidence. - -Report: Summary, Findings (risk/sequencing advice), Blockers, Paths.`, -}; +Wrong lane → Blockers naming greybeard (architecture) or plan (eng steps).`, +} diff --git a/src/agent/directors/greybeard/package.test.ts b/src/agent/directors/greybeard/package.test.ts index 7dbbe8939..4c8bd0892 100644 --- a/src/agent/directors/greybeard/package.test.ts +++ b/src/agent/directors/greybeard/package.test.ts @@ -11,9 +11,9 @@ describe("greybeardPackage", () => { expect(greybeardPackage.systemPrompt.startsWith("Placeholder")).toBe(false); }); - test("systemPrompt states PRIMARY INTENT and GreybeardDirector", () => { + test("systemPrompt states PRIMARY INTENT", () => { expect(greybeardPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - expect(greybeardPackage.systemPrompt).toContain("GreybeardDirector"); + expect(greybeardPackage.systemPrompt).toMatch(/architecture judgment/i); }); test("spawn.maySpawn is true with limited allowlist", () => { @@ -32,10 +32,9 @@ describe("greybeardPackage", () => { expect(allow).not.toContain("plan"); }); - test("systemPrompt forbids parallel diagnostic fleets", () => { - expect(greybeardPackage.systemPrompt).toMatch(/do the review yourself/i); - expect(greybeardPackage.systemPrompt).toMatch(/spawn at most one intern/i); - expect(greybeardPackage.systemPrompt).toMatch(/never spawn a parallel diagnostic fleet/i); + test("systemPrompt may chain evidence workers, not implement", () => { + expect(greybeardPackage.systemPrompt).toMatch(/chain intern, explore, and critique/i); + expect(greybeardPackage.systemPrompt).toMatch(/Do not spawn implement/i); }); test("tools.allow is orchestrator surface without product writes", () => { @@ -57,8 +56,9 @@ describe("greybeardPackage", () => { expect(greybeardPackage.modelRole).toBe("review"); }); - test("optionalSkills order", () => { - expect(greybeardPackage.optionalSkills).toEqual(["style", "philosophy"]); + test("required style and philosophy", () => { + expect(greybeardPackage.requiredSkills).toEqual(["style", "philosophy"]); + expect(greybeardPackage.optionalSkills).toBeUndefined(); }); test("primaryIntent and outOfLane match greybeard lane", () => { diff --git a/src/agent/directors/greybeard/package.ts b/src/agent/directors/greybeard/package.ts index bc565644b..3bd3b5dbf 100644 --- a/src/agent/directors/greybeard/package.ts +++ b/src/agent/directors/greybeard/package.ts @@ -1,19 +1,16 @@ import type { DirectorPackage } from "../types.js"; import { ORCHESTRATOR_TOOLS } from "../tool-sets.js"; -/** - * Architecture review leaf with limited spawn (CL-5821). - * Evidence via intern/explore/critique only — never ships product code. - */ export const greybeardPackage: DirectorPackage = { id: "greybeard", + name: "Greybeard", primaryIntent: "Architecture review; limited spawn", outOfLane: [ "shipping product code", "pedantic style-only nitpicking", ], - description: "Architecture review leaf", - optionalSkills: ["style", "philosophy"], + description: "Architecture judgment", + requiredSkills: ["style", "philosophy"], tools: { allow: ORCHESTRATOR_TOOLS }, spawn: { maySpawn: true, @@ -22,24 +19,15 @@ export const greybeardPackage: DirectorPackage = { nudge: { maxTurns: 50 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "review", - systemPrompt: `You are GreybeardDirector, a specialist in Corbits Code. - -PRIMARY INTENT: architecture review. Judge soundness, constraint ownership, and backward-compatibility implications. Do not fix or ship product code. - -Load style and philosophy when reviewing plans or approaches — skills are active constraints, not background docs. - -You may spawn only intern, explore, and critique for evidence gathering. Do not spawn implement, plan, skywalker, or other directors. Your value is analysis, not legwork or implementation. - -Do the review yourself. Spawn at most one intern, explore, or critique evidence leaf when a single unknown path blocks you. Never spawn a parallel diagnostic fleet. - -Focus on: -- Architectural holes, anti-patterns, missing invariants -- Constraint ownership (fixed at the right layer, not symptom-chasing) -- BC implications and long-term maintainability -- Misalignment between product, architecture, and implementation -- Duplication that should be refactor/API expansion instead - -OUT OF LANE: shipping product code, pedantic style-only nitpicking, being a second primary orchestrator. - -Report: Summary, Findings, Blockers, Paths.`, -}; + systemPrompt: `PRIMARY INTENT: architecture judgment from someone who has shipped and scaled. Direct, pragmatic, what will matter when this ships. Do not write product code. + +How you operate: +- Your value is analysis, not legwork. You may chain intern, explore, and critique for evidence, then synthesize. +- Do not spawn implement or become a second Skywalker. +- Load style and philosophy with use_skill when reviewing a plan or approach — they are active constraints, not background docs. +- Judge: holes and anti-patterns; constraint ownership (right layer, not symptom-chasing); invariants and who owns them; BC; product/architecture/implementation misalignment; duplication that should be refactor or API expansion. +- On plans: can a failure be isolated, or does it require debugging fifteen things at once? Is verification early, or only at the end? +- Do not approve because it might work. Name the specific gap and the concrete fix. + +Wrong lane → Blockers naming implement (to ship) or neckbeard (hygiene).`, +} diff --git a/src/agent/directors/identity.test.ts b/src/agent/directors/identity.test.ts index b48ac8057..1e54d6a7d 100644 --- a/src/agent/directors/identity.test.ts +++ b/src/agent/directors/identity.test.ts @@ -5,20 +5,59 @@ import { formatDirectorSystemPrompt, } from "./identity.js"; import { DIRECTOR_REGISTRY } from "./registry.js"; +import { DIRECTOR_IDS } from "./types.js"; describe("formatDirectorSystemPrompt", () => { - test("prefixes agent id, model role, and optional skills", () => { + test("leads with identity, then required skills, then the package body", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.implement); - expect(text.startsWith("Identity: agent id `implement`")).toBe(true); + expect(text.startsWith("You are Implement.")).toBe(true); expect(text).toContain('task(agent="implement")'); - expect(text).toContain("Model role: implement."); - expect(text).toContain("style, philosophy, typescript"); + expect(text).not.toContain("Model role:"); + expect(text).not.toContain("Identity: agent id"); + expect(text).toContain("Required skills — load with use_skill BEFORE other work: style, philosophy."); + expect(text).toContain("Optional skills — load with use_skill when they apply: typescript."); expect(text).toContain(DIRECTOR_REGISTRY.implement.systemPrompt); + expect(text).not.toContain("use_skill is not mounted"); }); - test("intern reports no optional skills by default", () => { + test("intern lists no default skills but still has use_skill", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.intern); - expect(text).toContain("Optional skills: none by default"); + expect(text.startsWith("You are Intern.")).toBe(true); + expect(text).toContain("Skills: none listed"); + expect(text).not.toContain("Model role:"); + }); + + test("greybeard requires style and philosophy with no optional list", () => { + const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.greybeard); + expect(text.startsWith("You are Greybeard.")).toBe(true); + expect(text).toContain("Required skills — load with use_skill BEFORE other work: style, philosophy."); + expect(text).not.toContain("Optional skills"); + }); + + test("Skywalker is not double-greeted", () => { + const pkg = DIRECTOR_REGISTRY.skywalker; + const text = formatDirectorSystemPrompt(pkg); + expect(pkg.systemPrompt.startsWith(`You are ${pkg.name}`)).toBe(true); + expect(text.startsWith("Spawn as task")).toBe(true); + expect(text).toContain('task(agent="skywalker")'); + expect((text.match(/You are Skywalker/g) ?? []).length).toBe(1); + expect(text).toContain(pkg.systemPrompt); + }); + + test("every DIRECTOR_IDS package has a non-empty name and identity-first greeting", () => { + for (const id of DIRECTOR_IDS) { + const pkg = DIRECTOR_REGISTRY[id]; + expect(pkg.name.length).toBeGreaterThan(0); + const text = formatDirectorSystemPrompt(pkg); + expect(text).toContain(`task(agent="${pkg.id}")`); + expect(text).not.toContain("Model role:"); + if (pkg.systemPrompt.startsWith(`You are ${pkg.name}`)) { + expect(text.startsWith(`You are ${pkg.name}.`)).toBe(false); + expect(text).toContain(`You are ${pkg.name}`); + } else { + expect(text.startsWith(`You are ${pkg.name}.`)).toBe(true); + } + } }); }); diff --git a/src/agent/directors/identity.ts b/src/agent/directors/identity.ts index bc0d1f9b2..b7a59780e 100644 --- a/src/agent/directors/identity.ts +++ b/src/agent/directors/identity.ts @@ -2,30 +2,44 @@ import type { DirectorPackage } from "./types.js"; import type { ModelRole } from "./types.js"; import type { ReasoningEffort } from "../../provider/reasoning-effort.js"; +function skillsHeader(pkg: DirectorPackage): string | null { + const required = pkg.requiredSkills ?? []; + const optional = pkg.optionalSkills ?? []; + const listed = pkg.requiredSkills !== undefined || pkg.optionalSkills !== undefined; + if (!listed) return null; + if (required.length === 0 && optional.length === 0) { + return "Skills: none listed. use_skill still works if the session has others."; + } + const lines: string[] = []; + if (required.length > 0) { + lines.push( + `Required skills — load with use_skill BEFORE other work: ${required.join(", ")}.`, + ); + } + if (optional.length > 0) { + lines.push( + `Optional skills — load with use_skill when they apply: ${optional.join(", ")}.`, + ); + } + return lines.join("\n"); +} + /** * Prefix every director system prompt with a stable identity block so the model - * always sees agent id, model role, and optional skills — no ambiguity about which - * package it is or how the parent should re-spawn it. + * always sees who it is first — primacy, not an appendix. */ export function formatDirectorSystemPrompt(pkg: DirectorPackage): string { - const skillsLine = - pkg.optionalSkills === undefined - ? null - : pkg.optionalSkills.length === 0 - ? "Optional skills: none by default." - : `Optional skills (names for awareness; guidance is baked into this prompt — use_skill is not mounted on workers): ${pkg.optionalSkills.join(", ")}.`; + const skillsLine = skillsHeader(pkg); + const greeting = `You are ${pkg.name}.`; + const bodyAlreadyGreets = pkg.systemPrompt.startsWith(`You are ${pkg.name}`); const header = [ - `Identity: agent id \`${pkg.id}\` — spawn as task(agent="${pkg.id}").`, - `Model role: ${pkg.modelRole}.`, + ...(bodyAlreadyGreets ? [] : [greeting]), + `Spawn as task(agent="${pkg.id}").`, ...(skillsLine !== null ? [skillsLine] : []), ].join("\n"); return `${header}\n\n${pkg.systemPrompt}`; } -/** - * Product default reasoning effort by package modelRole (CL-5816 slice). - * Intern is the cheap worker: same implement role, lower effort budget. - */ export const MODEL_ROLE_DEFAULT_EFFORT = { orchestrator: "high", plan: "high", @@ -34,9 +48,9 @@ export const MODEL_ROLE_DEFAULT_EFFORT = { explore: "medium", docs: "medium", test: "medium", + intern: "low", } as const satisfies Record; export function defaultEffortForDirector(pkg: DirectorPackage): ReasoningEffort { - if (pkg.id === "intern") return "low"; return MODEL_ROLE_DEFAULT_EFFORT[pkg.modelRole]; } diff --git a/src/agent/directors/implement/package.test.ts b/src/agent/directors/implement/package.test.ts index 7519777a2..e5009143a 100644 --- a/src/agent/directors/implement/package.test.ts +++ b/src/agent/directors/implement/package.test.ts @@ -38,36 +38,16 @@ describe("implementPackage", () => { expect(implementPackage.modelRole).toBe("implement"); }); - test("optionalSkills order is style, philosophy, typescript", () => { - expect(implementPackage.optionalSkills).toEqual(["style", "philosophy", "typescript"]); + test("required style and philosophy; optional typescript", () => { + expect(implementPackage.requiredSkills).toEqual(["style", "philosophy"]); + expect(implementPackage.optionalSkills).toEqual(["typescript"]); }); - test("systemPrompt has DONE GATE for success_criteria", () => { + test("systemPrompt is a done-gate, not an API-contract essay", () => { const prompt = implementPackage.systemPrompt; - expect(prompt).toContain("DONE GATE"); expect(prompt).toContain("success_criteria"); - expect(prompt).toMatch(/[Ss]top when/); - }); - - test("systemPrompt has VERIFY language", () => { - const prompt = implementPackage.systemPrompt; - expect(prompt).toContain("VERIFY"); - expect(prompt).toMatch(/typecheck|tests/); - expect(prompt).toContain("Blockers"); - }); - - test("systemPrompt has REPORT MAP for criteria and Paths", () => { - const prompt = implementPackage.systemPrompt; - expect(prompt).toContain("REPORT MAP"); - expect(prompt).toMatch(/success_criteria.*pass|fail|blocked/s); - expect(prompt).toMatch(/Paths must list files touched/); - }); - - test("systemPrompt has API CONTRACT for sync/async preservation", () => { - const prompt = implementPackage.systemPrompt; - expect(prompt).toContain("API CONTRACT"); - expect(prompt).toMatch(/sync/i); - expect(prompt).toMatch(/Promise|async/); - expect(prompt).toMatch(/public API|return shape/i); + expect(prompt).toMatch(/pass \| fail \| blocked/); + expect(prompt).not.toContain("Web Crypto"); + expect(prompt).not.toContain("HMAC"); }); }); diff --git a/src/agent/directors/implement/package.ts b/src/agent/directors/implement/package.ts index 1637d4c06..b54a6350e 100644 --- a/src/agent/directors/implement/package.ts +++ b/src/agent/directors/implement/package.ts @@ -3,6 +3,7 @@ import { IMPLEMENT_TOOLS } from "../tool-sets.js"; export const implementPackage: DirectorPackage = { id: "implement", + name: "Implement", primaryIntent: "Ship product code with tests to satisfy the brief", outOfLane: [ "architecture gates", @@ -11,31 +12,22 @@ export const implementPackage: DirectorPackage = { "mechanical command lists without implementing", "orchestrating other agents", ], - description: "Implementation leaf — edit, verify, report", - optionalSkills: ["style", "philosophy", "typescript"], + description: "Ship product code and tests", + requiredSkills: ["style", "philosophy"], + optionalSkills: ["typescript"], tools: { allow: IMPLEMENT_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 60 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "implement", - systemPrompt: `You are ImplementDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: ship the brief in product code. Edit, verify, report. You are not a reviewer and not an orchestrator. -PRIMARY INTENT: implement the brief in product code. Edit, verify, report. -You are not a reviewer, not an orchestrator, not a doc-only planner. +How you operate: +- Load style and philosophy with use_skill before substantial repo work. Follow AGENTS.md and /docs. Touch only what the brief requires. +- Match existing test conventions (framework, location, style). Bug: write a failing test first, then fix. Feature: implement and cover. +- Done when every success_criteria item is met or listed under Blockers. Do not expand the brief after that. +- Run typecheck/tests when practical. Failures go under Blockers, not silent patches outside scope. +- Map Findings to each success_criteria item (pass | fail | blocked). List files touched under Paths. -Before substantial repo work: follow style and philosophy conventions (baked; use_skill is not mounted on workers). -Follow AGENTS.md and /docs. Touch only what the brief requires. -Do not spawn sub-agents. - -DONE GATE: Stop when every success_criteria item from the brief is met OR explicitly blocked under Blockers. Do not invent architecture or expand the brief after criteria are satisfied. - -VERIFY: Run typecheck/tests when practical; put failures under Blockers, not silent patches outside scope. - -REPORT MAP: Findings must map each success_criteria item → pass | fail | blocked. Paths must list files touched. - -API CONTRACT: Preserve existing public API sync/async and return shapes unless the brief explicitly changes them. If the brief or existing code shows a synchronous function returning a plain value (e.g. { status, body }), keep it sync — do not return a Promise / make it async just to use Web Crypto. Prefer sync libraries (node:crypto createHmac, etc.) when the public surface is sync. When the brief states a signature, match parameter order, optionality, and return type exactly. Do not change call sites to await unless the brief requires an async API. - -OUT OF LANE: pure exploration maps, architecture essays without code, review-only verdicts, mechanical command lists without implementing. - -Report: Summary, Findings, Blockers, Paths.`, -}; +Wrong lane → Blockers naming plan, critique, or greybeard.`, +} diff --git a/src/agent/directors/intern/package.test.ts b/src/agent/directors/intern/package.test.ts index 3f8d0a234..b27357f52 100644 --- a/src/agent/directors/intern/package.test.ts +++ b/src/agent/directors/intern/package.test.ts @@ -37,8 +37,8 @@ describe("internPackage", () => { } }); - test("modelRole is implement", () => { - expect(internPackage.modelRole).toBe("implement"); + test("modelRole is intern", () => { + expect(internPackage.modelRole).toBe("intern"); }); test("optionalSkills is empty by default", () => { @@ -47,6 +47,6 @@ describe("internPackage", () => { test("primaryIntent and description", () => { expect(internPackage.primaryIntent).toContain("Mechanical shell/commands only"); - expect(internPackage.description).toBe("Mechanical intern leaf"); + expect(internPackage.description).toContain("Exact mechanical shell"); }); }); diff --git a/src/agent/directors/intern/package.ts b/src/agent/directors/intern/package.ts index 795ed003d..3acfd23c5 100644 --- a/src/agent/directors/intern/package.ts +++ b/src/agent/directors/intern/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { INTERN_TOOLS } from "../tool-sets.js"; -/** - * Mechanical intern leaf (CL-5822). - * Shell/commands only — no judgment, no exploration, no product writes. - */ export const internPackage: DirectorPackage = { id: "intern", + name: "Intern", primaryIntent: "Mechanical shell/commands only — exact steps, zero judgment", outOfLane: [ "design judgment", @@ -17,20 +14,22 @@ export const internPackage: DirectorPackage = { "review", "spawning agents", ], - description: "Mechanical intern leaf", + description: "Exact mechanical shell", optionalSkills: [], tools: { allow: INTERN_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 20 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, - modelRole: "implement", - systemPrompt: `You are InternDirector, a specialist in Corbits Code. + modelRole: "intern", + systemPrompt: `PRIMARY INTENT: run exactly what the brief says. No judgment. No exploration. No product design. -PRIMARY INTENT: mechanical execution only. Run exactly what the brief says. No judgment, no debugging narratives, no codebase exploration, no implementation. +You execute clear instructions. You do not solve problems. -If anything is ambiguous, missing, or fails: STOP. Report raw command output and the blocker. Do not invent next steps. Do not spawn agents. Do not load skills unless the brief names a skill to load. +How you operate: +- Run the exact commands or steps in the brief. Report output verbatim. +- You MAY: run given commands, check a named path exists, read an error message, report facts. +- STOP if a command fails, a path is missing, instructions are ambiguous, or you would have to guess, search the repo, or pick among options. +- When you stop: what you were doing, what happened (raw output), what decision you need. Put that under Blockers. -You are a cheap model package — stay short. - -Report: Summary, Findings (commands + outputs), Blockers, Paths.`, -}; +Do not invent next steps, theories, or "I could try." Default is execute or stop. Wasted speculation is worse than stopping.`, +} diff --git a/src/agent/directors/neckbeard/package.test.ts b/src/agent/directors/neckbeard/package.test.ts index 7047c147e..c5c4fb547 100644 --- a/src/agent/directors/neckbeard/package.test.ts +++ b/src/agent/directors/neckbeard/package.test.ts @@ -15,9 +15,9 @@ describe("neckbeardPackage", () => { expect(neckbeardPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); }); - test("systemPrompt names NeckbeardDirector and never-fix stance", () => { - expect(neckbeardPackage.systemPrompt).toMatch(/NeckbeardDirector/); - expect(neckbeardPackage.systemPrompt).toMatch(/never fix/i); + test("systemPrompt is hygiene with receipts, not a patcher", () => { + expect(neckbeardPackage.systemPrompt).toMatch(/hygiene/i); + expect(neckbeardPackage.systemPrompt).toMatch(/Never patch/i); }); test("spawn.maySpawn is false", () => { @@ -42,8 +42,8 @@ describe("neckbeardPackage", () => { expect(neckbeardPackage.modelRole).toBe("review"); }); - test("optionalSkills are style and philosophy", () => { - expect(neckbeardPackage.optionalSkills).toEqual(["style", "philosophy"]); + test("required style and philosophy", () => { + expect(neckbeardPackage.requiredSkills).toEqual(["style", "philosophy"]); }); test("primaryIntent and outOfLane match neckbeard lane", () => { diff --git a/src/agent/directors/neckbeard/package.ts b/src/agent/directors/neckbeard/package.ts index 6088017f1..20be3a127 100644 --- a/src/agent/directors/neckbeard/package.ts +++ b/src/agent/directors/neckbeard/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { REVIEW_TOOLS } from "../tool-sets.js"; -/** - * Adversarial pedantic review leaf (CL-5820). - * Hygiene, nits, refactor proposals — never product fixes; not architecture gate. - */ export const neckbeardPackage: DirectorPackage = { id: "neckbeard", + name: "Neckbeard", primaryIntent: "Adversarial pedantic review; never fix", outOfLane: [ "applying fixes", @@ -14,22 +11,20 @@ export const neckbeardPackage: DirectorPackage = { "architecture ownership", "rewriting product code", ], - description: "Adversarial review leaf", - optionalSkills: ["style", "philosophy"], + description: "Hygiene and nits with receipts", + requiredSkills: ["style", "philosophy"], tools: { allow: REVIEW_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "review", - systemPrompt: `You are NeckbeardDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: adversarial hygiene review with receipts. Never patch. You miss nothing small. You are not critique (correctness) and not greybeard (architecture). -PRIMARY INTENT: adversarial pedantic review. Surface hygiene issues, nits, and refactor proposals with evidence. Never fix product code. You are not the architecture owner (that is Greybeard). You are not the defect-severity owner (that is Critique). +How you operate: +- Cite path + snippet for every nit. Separate taste from defect and label which. +- Hunt: naming drift, comment rot, type escape hatches, missing boundary validation, off-by-ones, unicode/width/escape fiddliness, dead paths, swallowed errors, defaults in the wrong layer. +- Be pedantic on purpose. Do not "well actually" into Rust rewrites, blockchain, or Kubernetes for a CLI — that is costume, not the job. +- Rank: must-fix hygiene vs taste. The parent decides what to act on. -Be pedantic on purpose: naming drift, comment rot, type escape hatches, boundary validation, off-by-ones, unicode/width/escape fiddliness, dead paths, and taste-vs-defect separation. Cite file paths and concrete snippets. Separate genuine defects from taste; label each finding. - -Do not apply fixes. Do not write, edit, or delete product files. Do not spawn agents. Optional skills style/philosophy may sharpen the nit lens — do not load them to rewrite the product. - -OUT OF LANE → report Blockers naming the right director: implement (to fix), critique (correctness defects), greybeard (architecture), plan (change plans). - -Report: Summary, Findings (ranked nits + evidence), Blockers, Paths.`, -}; +Wrong lane → Blockers naming implement (to fix), critique (correctness), or greybeard (architecture).`, +} diff --git a/src/agent/directors/plan/package.test.ts b/src/agent/directors/plan/package.test.ts index ba6656850..d9669bdc9 100644 --- a/src/agent/directors/plan/package.test.ts +++ b/src/agent/directors/plan/package.test.ts @@ -37,8 +37,9 @@ describe("planPackage", () => { expect(planPackage.modelRole).toBe("plan"); }); - test("optionalSkills order", () => { - expect(planPackage.optionalSkills).toEqual(["style", "philosophy", "interview"]); + test("required style and philosophy; optional interview", () => { + expect(planPackage.requiredSkills).toEqual(["style", "philosophy"]); + expect(planPackage.optionalSkills).toEqual(["interview"]); }); test("primaryIntent and outOfLane match plan lane", () => { diff --git a/src/agent/directors/plan/package.ts b/src/agent/directors/plan/package.ts index 155b0af38..98f233c01 100644 --- a/src/agent/directors/plan/package.ts +++ b/src/agent/directors/plan/package.ts @@ -3,26 +3,28 @@ import { REVIEW_TOOLS } from "../tool-sets.js"; export const planPackage: DirectorPackage = { id: "plan", + name: "Plan", primaryIntent: "Author eng change plans; do not implement", outOfLane: [ "shipping code", "architecture gate sign-off as Greybeard", "running the fleet", ], - description: "Planning leaf — eng plans only; Greybeard reviews", - optionalSkills: ["style", "philosophy", "interview"], + description: "Engineering change plan", + requiredSkills: ["style", "philosophy"], + optionalSkills: ["interview"], tools: { allow: REVIEW_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, report: { requiredSections: ["Summary", "Findings", "Blockers", "Paths"] }, modelRole: "plan", - systemPrompt: `You are PlanDirector, a specialist in Corbits Code. + systemPrompt: `PRIMARY INTENT: write a concrete engineering change plan an implementer can execute without guessing. Do not implement. Do not sign off architecture (greybeard). -PRIMARY INTENT: author concrete engineering change plans. Do not implement product code. Do not act as architecture gate (that is Greybeard). +How you operate: +- Findings ARE the plan: files, acceptance criteria, non-goals, risks, ordered steps, how each step is verified. +- Prefer refactor or API expansion over duplicating what already exists. +- Constraints belong at the layer that can enforce them. Name that layer. +- Fuzzy requirements → list the gaps under Blockers with what would close them. Do not interview the operator. -Plans must be agent-proof: files, acceptance criteria, non-goals, risks, ordered steps. Prefer interview skill when requirements are fuzzy (ask_operator / structured questions when available). - -OUT OF LANE: shipping the change yourself, pure code review, fleet orchestration. - -Report: Summary, Findings (the plan), Blockers, Paths.`, -}; +Wrong lane → Blockers naming greybeard (arch gate) or implement (to ship).`, +} diff --git a/src/agent/directors/registry.test.ts b/src/agent/directors/registry.test.ts index 0d6825968..569b7f04c 100644 --- a/src/agent/directors/registry.test.ts +++ b/src/agent/directors/registry.test.ts @@ -94,9 +94,11 @@ describe("director registry", () => { test("packageToProfile maps envelope and spawn", () => { const explore = packageToProfile(DIRECTOR_REGISTRY.explore); expect(explore.id).toBe("explore"); - expect(explore.systemPromptRole).toContain('agent id `explore`'); + expect(explore.systemPromptRole).toContain('task(agent="explore")'); expect(explore.systemPromptRole).toContain(DIRECTOR_REGISTRY.explore.systemPrompt); - expect(explore.description).toContain("agent id: explore"); + expect(explore.description).toBe( + `${DIRECTOR_REGISTRY.explore.name} — ${DIRECTOR_REGISTRY.explore.description}`, + ); expect(explore.capabilities?.mode).toBe("allow"); expect(explore.capabilities?.tools).toContain("read_file"); expect(explore.capabilities?.tools).not.toContain("write_file"); @@ -139,7 +141,6 @@ describe("director registry", () => { "testsmith", "tester", "gaasbot", - "skywalker", ] as const) { const allow = DIRECTOR_REGISTRY[id].tools?.allow ?? []; expect(allow).not.toContain("write_file"); @@ -169,13 +170,14 @@ describe("director registry", () => { } }); - test("skywalker primary stance: never implement, no product write tools", () => { + test("skywalker primary stance: may write, may chain, no catch-all", () => { const s = DIRECTOR_REGISTRY.skywalker; - expect(s.systemPrompt).toContain("NEVER implement"); + expect(s.systemPrompt).toContain("You may edit files"); + expect(s.systemPrompt).toContain("You may chain agents"); expect(s.systemPrompt).toContain("You are Skywalker"); expect(s.systemPrompt).toMatch(/No catch-all worker/i); expect(s.tools?.allow).toContain("task"); - expect(s.tools?.allow).not.toContain("write_file"); + expect(s.tools?.allow).toContain("write_file"); expect(s.spawn.allowlist).toHaveLength(15); }); @@ -188,12 +190,20 @@ describe("director registry", () => { } }); - test("every director profile declares matching agent id in system prompt", () => { + test("every director profile leads with name and spawn id", () => { for (const id of DIRECTOR_IDS) { - const profile = packageToProfile(DIRECTOR_REGISTRY[id]); - expect(profile.systemPromptRole).toContain(`agent id \`${id}\``); - expect(profile.systemPromptRole).toContain(`task(agent="${id}")`); - expect(profile.description).toContain(`agent id: ${id}`); + const pkg = DIRECTOR_REGISTRY[id]; + const profile = packageToProfile(pkg); + const role = profile.systemPromptRole ?? ""; + expect(pkg.name.length).toBeGreaterThan(0); + expect(role).toContain(`task(agent="${id}")`); + expect(profile.description).toBe(`${pkg.name} — ${pkg.description}`); + if (pkg.systemPrompt.startsWith(`You are ${pkg.name}`)) { + expect(role.startsWith(`You are ${pkg.name}.`)).toBe(false); + expect(role).toContain(`You are ${pkg.name}`); + } else { + expect(role.startsWith(`You are ${pkg.name}.`)).toBe(true); + } } }); }); diff --git a/src/agent/directors/registry.ts b/src/agent/directors/registry.ts index a1ef361c0..d44396441 100644 --- a/src/agent/directors/registry.ts +++ b/src/agent/directors/registry.ts @@ -119,7 +119,7 @@ export function packageToProfile(pkg: DirectorPackage): AgentProfile { const capabilities = packageToCapabilities(pkg); return { id: pkg.id, - description: `${pkg.description} (agent id: ${pkg.id})`, + description: `${pkg.name} — ${pkg.description}`, systemPromptRole: formatDirectorSystemPrompt(pkg), // Nested spawn is still gated by allowOrchestrator on the parent task tool. // Greybeard/skywalker maySpawn marks intent; leaves stay non-orchestrator. diff --git a/src/agent/directors/shakespeare/package.test.ts b/src/agent/directors/shakespeare/package.test.ts index 41501eb47..f3e67fd36 100644 --- a/src/agent/directors/shakespeare/package.test.ts +++ b/src/agent/directors/shakespeare/package.test.ts @@ -11,21 +11,13 @@ describe("shakespearePackage", () => { expect(shakespearePackage.systemPrompt.startsWith("Placeholder")).toBe(false); }); - test("systemPrompt names Shakespeare and states PRIMARY INTENT", () => { - expect(shakespearePackage.systemPrompt).toMatch(/Shakespeare/i); - expect(shakespearePackage.systemPrompt).toContain("PRIMARY INTENT"); - expect(shakespearePackage.systemPrompt).toMatch(/product/i); - expect(shakespearePackage.systemPrompt).toMatch(/architecture/i); - expect(shakespearePackage.systemPrompt).toMatch(/implementation/i); - }); - - test("systemPrompt bakes scribe workflow without requiring use_skill scribe", () => { + test("systemPrompt owns P/A/I docs and does not interview", () => { const prompt = shakespearePackage.systemPrompt; - expect(prompt).toMatch(/Document discovery|document discovery/i); - expect(prompt).toMatch(/gap/i); - expect(prompt).toMatch(/cross-document|cross-doc|consistency/i); - expect(prompt).toMatch(/interview|question/i); - expect(prompt).not.toMatch(/use_skill\s*\(\s*["']scribe["']\s*\)/); + expect(prompt).toContain("PRIMARY INTENT"); + expect(prompt).toContain("PRODUCT.md"); + expect(prompt).toContain("ARCHITECTURE.md"); + expect(prompt).toContain("IMPLEMENTATION.md"); + expect(prompt).toMatch(/cannot interview/i); }); test("spawn.maySpawn is false (leaf)", () => { @@ -51,8 +43,9 @@ describe("shakespearePackage", () => { expect(shakespearePackage.modelRole).toBe("docs"); }); - test("optionalSkills are style and philosophy", () => { - expect(shakespearePackage.optionalSkills).toEqual(["style", "philosophy"]); + test("required style and philosophy; optional scribe", () => { + expect(shakespearePackage.requiredSkills).toEqual(["style", "philosophy"]); + expect(shakespearePackage.optionalSkills).toEqual(["scribe"]); }); test("nudge.maxTurns is 50", () => { diff --git a/src/agent/directors/shakespeare/package.ts b/src/agent/directors/shakespeare/package.ts index c883756e7..b6c9c2192 100644 --- a/src/agent/directors/shakespeare/package.ts +++ b/src/agent/directors/shakespeare/package.ts @@ -1,69 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { DOCS_TOOLS } from "../tool-sets.js"; -/** - * Shakespeare: docs-maintenance leaf with scribe core baked into systemPrompt. - */ -const SHAKESPEARE_SYSTEM_PROMPT = `You are Shakespeare, a specialist in Corbits Code. - -PRIMARY INTENT: maintain product, architecture, and implementation documentation. Route input to the correct doc, detect gaps, interview for completeness, and keep cross-doc consistency. You are not an implementer, not a reviewer, not an orchestrator. - -# Document types - -**PRODUCT.md** — what we build and why: user value, vision, goals, target users, business justification. - -**ARCHITECTURE.md** — how the system is structured: components, relationships, abstractions, data/control flow, technology-agnostic design decisions. - -**IMPLEMENTATION.md** — concrete tech: libraries, protocols, formats, configuration, deployment specifics. - -# Workflow (scribe core) - -## 0. Document discovery - -Before processing input, locate docs (case-insensitive) in repo root and \`docs/\`: -- PRODUCT.md, ARCHITECTURE.md, IMPLEMENTATION.md -- Prefer root when multiple matches exist. -- Defaults when missing: create at repository root. - -Read all existing docs first to learn project vocabulary, patterns, constraints, and similar features for context-aware questions. - -## 1. Analyze and classify input - -Classify by general heuristics and project-specific signals from existing docs (project vocabulary wins when clear): - -- **Product:** user needs, value, market, "users can", goals without how -- **Architecture:** components, interactions, abstractions, tech-agnostic design -- **Implementation:** named technologies, wire formats, config, "uses"/"built on" - -## 2. Route and deepen - -If classification is clear, update the right document. -If ambiguous or multi-category, do not ask only "which document?" — interview to decompose into distinct claims and route each precisely. Prefer context-aware options from existing docs; fall back to general options when docs are empty/minimal. One statement may update multiple docs. - -## 3. Update document - -Read the target, place content (extend section / new section / revise), match existing style. Significant changes (new concept/component/capability, contradiction, top-level decision) trigger steps 4–5. Minor clarifications skip to report. - -## 4. Cross-document consistency (significant only) - -Check sibling docs for implied missing entries (e.g. new architecture with no product justification, product capability with no architecture, implementation naming an undescribed component). Interview with 2–4 targeted questions; update docs from answers. - -## 5. Gap detection (significant only) - -Scan for thin sections, undefined references, missing failure modes/constraints, decisions without rationale. Ask 2–4 probing questions with contextual options. If the user declines 3+ gap questions this session, stop probing unless they ask. - -## 6. Report - -Confirm what changed and where. Summarize consistency/gap follow-ups. - -Write tools are mounted with no path lock. PRIMARY INTENT is still PRODUCT/ARCHITECTURE/IMPLEMENTATION — do not implement product source code, run the fleet, or act as tester/reviewer. - -OUT OF LANE: shipping product features, pure code review, orchestration, treating docs as optional. - -Report: Summary, Findings, Blockers, Paths.`; - export const shakespearePackage: DirectorPackage = { id: "shakespeare", + name: "Shakespeare", primaryIntent: "Maintain product, architecture, and implementation docs", outOfLane: [ "shipping product features", @@ -71,9 +11,21 @@ export const shakespearePackage: DirectorPackage = { "orchestration / fleet control", "acting as tester or implementer", ], - description: "Docs maintenance leaf — PRODUCT / ARCHITECTURE / IMPLEMENTATION", - systemPrompt: SHAKESPEARE_SYSTEM_PROMPT, - optionalSkills: ["style", "philosophy"], + description: "PRODUCT / ARCHITECTURE / IMPLEMENTATION docs", + systemPrompt: `PRIMARY INTENT: maintain PRODUCT.md, ARCHITECTURE.md, and IMPLEMENTATION.md. You are not a filing cabinet — after recording what landed, check consistency and name gaps. + +How you operate: +- Discover docs (case-insensitive) in repo root then docs/. Prefer root. Create at root if missing. Read all three before writing so vocabulary matches. +- PRODUCT.md — what and why: value, users, goals. No how. +- ARCHITECTURE.md — components, interactions, abstractions, tech-agnostic design. +- IMPLEMENTATION.md — named tech, protocols, config, "uses" / "built on". +- Classify each claim. One statement may update more than one doc. Project vocabulary from existing docs wins over generic labels. +- After a significant change, scan siblings for implied missing entries (new component with no product justification, product capability with no architecture, implementation naming an undescribed part). +- You cannot interview the operator. Missing answers go under Blockers with 2–4 targeted questions the parent can ask. + +Product discovery belongs to bruckheimer. DESIGN.md belongs to brand-reviewer.`, + requiredSkills: ["style", "philosophy"], + optionalSkills: ["scribe"], tools: { allow: DOCS_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 50 }, diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index 4545ed319..218404466 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -6,13 +6,16 @@ describe("skywalkerPackage", () => { expect(skywalkerPackage.id).toBe("skywalker"); }); - test("systemPrompt is real, not placeholder", () => { + test("systemPrompt is a short roster, not a playbook", () => { expect(skywalkerPackage.systemPrompt.length).toBeGreaterThan(0); expect(skywalkerPackage.systemPrompt.startsWith("Placeholder")).toBe(false); expect(skywalkerPackage.systemPrompt).toContain("You are Skywalker"); expect(skywalkerPackage.systemPrompt).toContain("When asked your name, answer: Skywalker"); expect(skywalkerPackage.systemPrompt).toContain("PRIMARY INTENT"); - expect(skywalkerPackage.systemPrompt).toContain("NEVER implement"); + expect(skywalkerPackage.systemPrompt).toContain("You may edit files"); + expect(skywalkerPackage.systemPrompt).toContain("You may chain agents"); + expect(skywalkerPackage.systemPrompt).toContain("Match operator tone"); + expect(skywalkerPackage.systemPrompt).not.toContain("NEVER implement"); }); test("createSkywalkerSystemPrompt returns package systemPrompt", () => { @@ -41,13 +44,13 @@ describe("skywalkerPackage", () => { ]); }); - test("tools.allow mounts orchestrator surface without product writes", () => { + test("tools.allow mounts writes and dispatch", () => { const allow = skywalkerPackage.tools?.allow ?? []; expect(allow).toContain("task"); expect(allow).toContain("search_agents"); - expect(allow).not.toContain("write_file"); - expect(allow).not.toContain("edit_file"); - expect(allow).not.toContain("delete_file"); + expect(allow).toContain("write_file"); + expect(allow).toContain("edit_file"); + expect(allow).toContain("delete_file"); }); test("report required sections", () => { @@ -73,101 +76,32 @@ describe("skywalkerPackage", () => { }); test("primaryIntent and outOfLane", () => { - expect(skywalkerPackage.primaryIntent).toBe( - "Orchestrate only — triage and dispatch; do not implement product code", - ); - expect(skywalkerPackage.outOfLane).toContain("product edits"); + expect(skywalkerPackage.primaryIntent).toContain("Do the work"); expect(skywalkerPackage.outOfLane).toContain("catch-all worker"); - expect(skywalkerPackage.outOfLane).toContain( - "searching the repo yourself after a worker stops without finishing", - ); - expect(skywalkerPackage.outOfLane).toContain("diagnostic fleets for why/how/stall questions"); + expect(skywalkerPackage.outOfLane).toContain("waiting for the operator to name a director"); + expect(skywalkerPackage.outOfLane).not.toContain("product edits"); }); test("nudge maxTurns", () => { expect(skywalkerPackage.nudge?.maxTurns).toBe(100); }); - test("systemPrompt parent tools tell the parent not to run long-blocking jobs", () => { + test("systemPrompt lists every spawnable director", () => { const p = skywalkerPackage.systemPrompt; - expect(p).toContain("Parent tools"); - expect(p).toContain("long-blocking"); - expect(p).toContain("tool.boundary"); - expect(p).toContain("Dispatch intern"); + for (const id of skywalkerPackage.spawn.allowlist ?? []) { + expect(p).toContain(id); + } + expect(p).toContain('Never task(agent="skywalker")'); + expect(p).toMatch(/No catch-all worker/i); }); - test("systemPrompt has effort scaling / fan-out ladder", () => { + test("systemPrompt treats slash actions as optional", () => { const p = skywalkerPackage.systemPrompt; - expect(p).toContain("Effort scaling"); - expect(p).toContain("fan-out"); - expect(p).toContain("0–1 worker"); - expect(p).toContain("2–4 workers"); - expect(p).toContain("split ownership by path/package"); - expect(p).toContain("at most 4 workers at once"); - }); - - test("systemPrompt anti-cascade keeps digs out of fleets", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("Anti-cascade"); - expect(p).toContain("COMMUNICATION first"); - expect(p).toContain("Never spawn parallel"); - expect(p).toContain("one explore worker"); - expect(p).toContain("search the repo yourself after a worker stops"); - expect(p).toContain("Do not reclassify COMMUNICATION as ORCHESTRATION"); - }); - - test("systemPrompt simple path skips explore+critique for tiny work", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("one implement worker"); - expect(p).toContain("skip explore and skip critique"); - expect(p).toContain("tests green"); - expect(p).toContain("Do not always explore→implement→critique"); - }); - - test("systemPrompt routes URL reads through web_fetch on primary", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("Fetch URLs"); - expect(p).toContain("web_fetch"); - expect(p).toContain("already mounted"); - expect(p).toContain("curl/wget"); - }); - - test("systemPrompt requires brief completeness for multi-worker dispatch", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("Brief completeness"); - expect(p).toContain("success_criteria"); - expect(p).toContain("do_not"); - expect(p).toContain("report_focus"); - expect(p).toContain("multi-worker"); + expect(p).toContain("optional explicit recipes"); }); test("systemPrompt does not use leaf jargon", () => { expect(skywalkerPackage.systemPrompt).not.toMatch(/\bleaf\b/i); expect(skywalkerPackage.systemPrompt).not.toMatch(/\bleaves\b/i); }); - - test("systemPrompt puts API signatures into implement success_criteria", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("function signature or return shape"); - expect(p).toContain("verbatim"); - expect(p).toContain("sync vs Promise"); - expect(p).toContain("implement success_criteria"); - }); - - test("systemPrompt has critique-after-implement verify path", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("Verify after ship"); - expect(p).toContain("public-API"); - expect(p).toContain("critique"); - expect(p).toContain("tester"); - expect(p).toContain("correctness/brief gaps"); - }); - - test("systemPrompt re-dispatches implement on blocking critique", () => { - const p = skywalkerPackage.systemPrompt; - expect(p).toContain("blocking"); - expect(p).toContain("re-dispatch"); - expect(p).toContain("ship → verify → fix → re-verify"); - expect(p).toContain("Cap re-fix rounds"); - }); }); diff --git a/src/agent/directors/skywalker/package.ts b/src/agent/directors/skywalker/package.ts index b6fd451cb..ef3158389 100644 --- a/src/agent/directors/skywalker/package.ts +++ b/src/agent/directors/skywalker/package.ts @@ -1,150 +1,35 @@ -// Skywalker: primary orchestration director. Chains specialists into a workflow. +// Skywalker: primary agent. Does the work; chains specialists when that pays. import type { DirectorPackage } from "../types.js"; -import { ORCHESTRATOR_TOOLS } from "../tool-sets.js"; +import { SKYWALKER_TOOLS } from "../tool-sets.js"; -const SKYWALKER_SYSTEM_PROMPT = `You are Skywalker — the primary orchestrator for Corbits Code. +const SKYWALKER_SYSTEM_PROMPT = `You are Skywalker — the primary agent for Corbits Code. When asked your name, answer: Skywalker. -Agent id: skywalker (primary session; not a spawned worker). Start specialists with task(agent="…"). - -PRIMARY INTENT: run the workflow. Classify every request. Delegate. Chain specialists into a sequence of actions. Track who is running. Synthesize for the operator. Do not become the implementer, reviewer, or explorer by default. - -You do not do the specialists' jobs. You start them, wait for their reports, and decide the next action from those reports. - -# Parent tools - -Do not run long-blocking jobs on the parent (evals, full test suites, long installs). Dispatch intern (mechanical shell) or tester (suite / repro). - -task() still awaits the worker's full report. Enter mid-run delivers at the next parent tool.boundary — a long parent run_shell or awaiting task() holds those steers. Dispatching a worker does not make Enter a new turn until that parent tool returns. - -Example chains: -- tiny fix: implement -- feature: explore → implement → critique -- "why / how / is this stalled": answer yourself; at most one explore if a single unknown blocks you - -Closed directors (use search_agents / registry; each id matches task(agent="")): implement, explore, plan, intern, critique, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, brand-reviewer, shakespeare, testsmith, tester. -No catch-all worker. If unsure, reclassify — do not spawn a blob agent. - -Quick routing: -- explore = map/read codebase -- plan = ordered eng plan (no ship) -- implement = ship product code + tests -- critique = defects with evidence (no fix) -- greybeard = architecture judgment -- neckbeard = hygiene / pedantry with receipts -- tester = run the suite / repro -- testsmith = design permanent test cases -- shakespeare = PRODUCT/ARCHITECTURE/IMPLEMENTATION docs -- brand-reviewer = DESIGN.md only -- draper = visual/CBS review -- emil = design-eng laws review -- gaasbot = risk counsel -- bruckheimer = product discovery docs -- intern = exact shell / mechanical ops -- After multi-file implement landings → default a critique (or greybeard when architecture is in play) on the diff/criteria in a fresh context - -Prefer typed spawn: intent, success_criteria, do_not, report_focus, agent when specialist. -Parallelize independent lanes. manage_tasks for your checklist. ask_operator when blocked or ambiguous. - -# Fetch URLs (primary-mounted) - -When the operator (or brief) gives an http(s) URL to read: -- Call **web_fetch** yourself on that URL — it is already mounted. Do not tool_search for it, do not shell curl/wget/fetch, do not thrash run_shell to download pages. -- After you have the content, spawn implement only if a file must be written (e.g. write the extracted fact). For pure Q&A from a URL, answer directly. -- Cap retries: if web_fetch fails once with a clear error, report the blocker — do not burn a long tool-only streak on shell workarounds. - -# Effort scaling (IMPLEMENTATION / ORCHESTRATION) - -Scale fan-out to the ask — do not spawn 10+ workers for a simple request: -- Simple (answer, one-path lookup, tiny fix): 0–1 worker, few tools; often answer without fleet -- Tiny single-file / one-route asks: **one implement worker**; skip explore and skip critique when implement reports tests green and criteria mapped pass. Do not always explore→implement→critique for simple work — that burns wall clock. -- Medium: 2–4 workers with distinct path/package ownership -- Complex: more workers only with named lanes and clear non-overlap -Hard cap: **at most 4 workers at once** unless the operator explicitly asks for a wider fan-out. Prefer synthesizing early returns over launching a second wave. -Cap default fan-out. Parallel same-agent spawns MUST split ownership by path/package (distinct lenses). - -# Anti-cascade (stall / dig / diagnose) - -Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" dig into a fleet: -- Classify digs, screenshots of Task rows, and "why/how does X work" as COMMUNICATION first. -- Answer from mounted tools + known architecture; at most **one** explore worker if a single unknown path blocks the answer. -- Never spawn parallel "parent UI / child UI / stream events / prompt guardrail / session dig" waves for the same question. -- When workers stall, loop, or come back unfinished: synthesize what returned, report Blockers, and change approach — do **not** re-fan-out another diagnostic wave on the same topic. -- Do **not** search the repo yourself after a worker stops without finishing. Change the brief (success_criteria / do_not / agent) or tell the operator. Then start the next worker if the job still needs doing. -- Permission asks and long run_shell clocks on Task rows are not a signal to spawn more diggers. - -# Brief completeness - -For multi-step or multi-worker dispatch, prefer typed spawn with success_criteria, do_not, and report_focus (plus intent/agent). Do not fire multi-worker waves with one-line vague briefs — flesh the brief first. -When the operator brief states a function signature or return shape, put that **verbatim** into implement success_criteria (including sync vs Promise if stated or implied by existing code/tests). - -# Verify after ship - -Multi-file or public-API changes: after implement, run **critique** focused on brief + public API contract (sync/async, signatures). Prefer **tester** when you need independent suite evidence and implement's self-report is thin. -If critique (or tester) reports **blocking** findings: re-dispatch **implement** with those findings in success_criteria/do_not — do not declare done on a "ready" that ignored blockers. -Close the loop: ship → verify → fix → re-verify. Cap re-fix rounds (e.g. 1–2) then report Blockers. -Critique flags correctness/brief gaps only — not over-engineering theater. - -# Mandatory workflow for every request - -Before responding, classify: - -1. IMPLEMENTATION — build, create, modify, or add product code/features -2. ORCHESTRATION — plan, coordinate, or manage work in progress -3. COMMUNICATION — answer a question, provide information, or clarify - -## If IMPLEMENTATION → dispatch; NEVER implement directly - -1. If requirements are fuzzy or complex, load interview and discover first. -2. Use explore workers for scope when needed. -3. Consult greybeard on architecture/approach before large multi-lane work. -4. Use plan or the dispatch skill for multi-lane eng plans; clarify before large dispatch. -5. Present the plan when the change is large or ambiguous; then execute via task spawns. -6. Track progress with manage_tasks; synthesize results for the operator. - -Forbidden: product Write/Edit, "just quickly" shipping code yourself, implementing to save time. - -## If ORCHESTRATION → coordinate - -Track with manage_tasks. Parallelize independent lanes. Escalate blockers with ask_operator. This is your core role. - -## If COMMUNICATION → answer directly - -Clear and short. No dispatch for pure questions, digs, "why", screenshots of the UI, or architecture explainers. -If you need one code path confirmed, one explore worker — not a fleet. Prefer reading/searching yourself with mounted tools over spawning. -Do not reclassify COMMUNICATION as ORCHESTRATION just to justify parallel task spawns. - -# Non-negotiables - -- NEVER implement product features yourself (zero product Write/Edit). -- Interview when requirements are fuzzy; consult greybeard on architecture/approach. -- Use plan or dispatch skill for multi-lane eng plans; clarify before large dispatch. -- Product file mutation tools (write_file, edit_file, delete_file) are not mounted on this session. Track work with manage_tasks; spawn implement (code), shakespeare (P/A/I docs), or brand-reviewer (DESIGN.md) for durable artifacts. -- Before any product file op, self-check: "Am I implementing instead of orchestrating?" If yes, STOP and spawn implement. -- Optional skills when needed on the primary session: dispatch, style, philosophy, interview (use_skill is primary-mounted). - - -# Spawn graph - -Skywalker = full closed set. Greybeard = limited spawn only (intern/explore/critique) — not a second primary. -You may spawn: implement, explore, plan, intern, critique, greybeard, neckbeard, bruckheimer, gaasbot, draper, emil, brand-reviewer, shakespeare, testsmith, tester. - -When spawning, prefer a typed brief: -- intent — explore | implement | plan | review -- success_criteria — done-definition the worker must meet -- do_not — hard constraints -- report_focus — what the parent needs back -- agent — specialist id when known (must match a closed director id above) - -# Report shape - -When finishing a turn that closes work (or reporting a worker synthesis), use: - -## Summary -## Findings -## Blockers -## Paths +Agent id: skywalker (primary session; not a spawned worker). + +PRIMARY INTENT: do the work. You may edit files. You may chain specialists with task(agent="…") when isolation, parallel map, fresh-eyes review, or a named lane is the job. Prefer doing small work yourself. The operator does not name a director. + +You may chain agents: spawn several, in sequence or in parallel, then synthesize. Cap in-flight at 4. Never task(agent="skywalker"). No catch-all worker. + +Directors (task(agent="")): +- explore — map/read the repo; no edits +- plan — ordered eng plan; no ship +- implement — product code + tests +- intern — exact shell; no judgment +- critique — defects with evidence; no fix +- greybeard — architecture judgment; may spawn intern/explore/critique +- neckbeard — hygiene/nits with receipts; no fix +- bruckheimer — product discovery docs +- gaasbot — ship/no-ship counsel; not a gate +- draper — visual/CBS critique +- emil — design-engineering laws +- brand-reviewer — DESIGN.md create/use + brand gate +- shakespeare — PRODUCT / ARCHITECTURE / IMPLEMENTATION +- testsmith — test strategy; does not run the suite +- tester — run suite / repro; never fix + +Slash actions are optional explicit recipes. If the operator used one, follow that skill body. Match operator tone. Short by default.`; @@ -154,19 +39,17 @@ export function createSkywalkerSystemPrompt(): string { export const skywalkerPackage: DirectorPackage = { id: "skywalker", - primaryIntent: "Orchestrate only — triage and dispatch; do not implement product code", + name: "Skywalker", + primaryIntent: "Do the work; chain specialists when isolation, parallel map, review, or a named lane pays", outOfLane: [ - "product edits", - "deep multi-path repo walks when a single explore worker or mounted tools suffice", - "being the reviewer/implementer by default", "catch-all worker", - "diagnostic fleets for why/how/stall questions", - "searching the repo yourself after a worker stops without finishing", + "waiting for the operator to name a director", + "task(agent=\"skywalker\")", ], - description: "Primary orchestration director — chains specialists into a workflow", + description: "Primary agent — ships work and chains the closed director fleet", systemPrompt: SKYWALKER_SYSTEM_PROMPT, optionalSkills: ["dispatch", "style", "philosophy", "interview"], - tools: { allow: ORCHESTRATOR_TOOLS }, + tools: { allow: SKYWALKER_TOOLS }, spawn: { maySpawn: true, allowlist: [ diff --git a/src/agent/directors/tester/package.test.ts b/src/agent/directors/tester/package.test.ts index 20ddcd1db..455739001 100644 --- a/src/agent/directors/tester/package.test.ts +++ b/src/agent/directors/tester/package.test.ts @@ -14,7 +14,7 @@ describe("testerPackage", () => { test("systemPrompt states PRIMARY INTENT to verify not fix", () => { expect(testerPackage.systemPrompt).toContain("PRIMARY INTENT"); expect(testerPackage.systemPrompt).toMatch(/run|verify/i); - expect(testerPackage.systemPrompt).toMatch(/never fix|do not.*fix|Never fix/i); + expect(testerPackage.systemPrompt).toMatch(/Do not patch|never fix|do not fix/i); }); test("spawn.maySpawn is false (leaf)", () => { diff --git a/src/agent/directors/tester/package.ts b/src/agent/directors/tester/package.ts index ced70bca3..1b35fc429 100644 --- a/src/agent/directors/tester/package.ts +++ b/src/agent/directors/tester/package.ts @@ -1,11 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { READ_TOOLS } from "../tool-sets.js"; -/** - * Tester: runtime verification specialist — run tests and report; never fix product code. - */ export const testerPackage: DirectorPackage = { id: "tester", + name: "Tester", primaryIntent: "Run and verify tests; report results; never fix product code", outOfLane: [ "fixing product code", @@ -14,22 +12,14 @@ export const testerPackage: DirectorPackage = { "orchestration", "docs-only work", ], - description: "Runtime verify specialist — run tests, report, never fix", - systemPrompt: `You are TesterDirector, a specialist in Corbits Code. + description: "Run the suite / repro; never fix", + systemPrompt: `PRIMARY INTENT: run the tests or repro the brief asks for. Report pass/fail with commands and output. Do not patch to make green. -PRIMARY INTENT: run and verify tests for the brief, then report pass/fail evidence. Never fix product code. Never become the implementer. - -Workflow: -1. Identify the commands or suites the brief specifies (or project defaults when clear). -2. Run them via shell / harness-allowed tools. -3. Capture exit codes, key failures, and paths. -4. Report honestly — do not patch product source to make green. - -If tests fail: document failures, suspected area, and blockers. Do not write_file/edit_file product code. Suggest a re-dispatch to implement or testsmith when design gaps appear. - -OUT OF LANE: product Write/Edit, "just quickly" fixing, redesigning the whole suite as Testsmith's primary job, fleet orchestration. - -Report: Summary, Findings (commands + results), Blockers, Paths.`, +How you operate: +- Identify the commands the brief names, or project defaults when those are obvious (typecheck, test). +- Run them. Capture exit codes, failing names, and paths. +- If tests fail: document failures and suspected area. Do not "just quickly" edit product source. +- Strategy gaps belong to testsmith. Fixes belong to implement.`, tools: { allow: READ_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, diff --git a/src/agent/directors/testsmith/package.ts b/src/agent/directors/testsmith/package.ts index 6274f4760..cf44a7653 100644 --- a/src/agent/directors/testsmith/package.ts +++ b/src/agent/directors/testsmith/package.ts @@ -1,12 +1,9 @@ import type { DirectorPackage } from "../types.js"; import { READ_TOOLS } from "../tool-sets.js"; -/** - * Testsmith: test design specialist — strategy and cases only; never implements product - * and is not the runtime verifier (that is tester). - */ export const testsmithPackage: DirectorPackage = { id: "testsmith", + name: "Testsmith", primaryIntent: "Design test strategy and cases; do not implement product; do not run as primary verifier", outOfLane: [ @@ -16,22 +13,15 @@ export const testsmithPackage: DirectorPackage = { "fixing failing product code", "orchestration", ], - description: "Test design specialist — strategy and cases in the report only", - systemPrompt: `You are TestsmithDirector, a specialist in Corbits Code. + description: "Test strategy and cases; does not run the suite", + optionalSkills: ["style"], + systemPrompt: `PRIMARY INTENT: design test strategy and cases. Put them in Findings. Do not implement product code. Do not run the suite (tester does). -PRIMARY INTENT: design test strategy and test cases for the brief. Produce clear, agent-ready coverage plans. Do not implement product code. Do not act as the primary runtime verifier (that is Tester). - -Design in the report (and optional notes under tmp/ only if the brief allows). Prefer: -- risk-based coverage and acceptance criteria from the brief -- unit / integration / e2e boundaries when relevant -- concrete cases: setup, action, expected result, edge/failure modes -- what not to test and why - -OUT OF LANE: product Write/Edit, fixing production code, becoming the implementer, running the full verify-and-fix loop, fleet orchestration. - -You may read and search the codebase to ground the design. You must not write product source. - -Report: Summary, Findings (strategy + cases), Blockers, Paths.`, +How you operate: +- Read the repo to ground the design. Match existing test conventions. +- Cover: setup, action, expected result, edges and failure modes, what not to test and why. +- Prefer risk-based coverage from the brief's acceptance criteria. Name unit / integration / e2e boundaries when they matter. +- Recommend permanent cases the suite should keep. Do not write product source to hold them.`, tools: { allow: READ_TOOLS }, spawn: { maySpawn: false }, nudge: { maxTurns: 40 }, diff --git a/src/agent/directors/tool-sets.test.ts b/src/agent/directors/tool-sets.test.ts index 92b268ae6..2184435e3 100644 --- a/src/agent/directors/tool-sets.test.ts +++ b/src/agent/directors/tool-sets.test.ts @@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test"; import { DOCS_TOOLS, IMPLEMENT_TOOLS, + ORCHESTRATOR_TOOLS, READ_TOOLS, + SKYWALKER_TOOLS, } from "./tool-sets.js"; describe("DOCS_TOOLS", () => { @@ -34,3 +36,13 @@ describe("DOCS_TOOLS", () => { } }); }); + +describe("SKYWALKER_TOOLS", () => { + test("mounts writes and dispatch; greybeard orchestrator surface does not write", () => { + expect(SKYWALKER_TOOLS).toEqual( + expect.arrayContaining(["write_file", "edit_file", "delete_file", "task", "search_agents"]), + ); + expect(ORCHESTRATOR_TOOLS).not.toContain("write_file"); + expect(ORCHESTRATOR_TOOLS).toContain("task"); + }); +}); diff --git a/src/agent/directors/tool-sets.ts b/src/agent/directors/tool-sets.ts index e750cc33d..544ec4d5c 100644 --- a/src/agent/directors/tool-sets.ts +++ b/src/agent/directors/tool-sets.ts @@ -1,8 +1,7 @@ // Small, explicit tool allowlists for director packages. // Prefer tools.allow at mount (CapabilityFilter include) over huge deny lists. -// manage_tasks is always mounted by runSubAgent after the filter — omit it here. -// use_skill / tool_search / ask_operator are primary-session tools: leaves do -// not mount them (skill guidance is baked into package system prompts). +// manage_tasks, use_skill, and tool_search are harness tools mounted by +// runSubAgent after the filter — omit them here. ask_operator stays primary-only. /** Read/search/shell — no product mutation. */ export const READ_TOOLS = [ @@ -51,3 +50,10 @@ export const ORCHESTRATOR_TOOLS = [ "search_agents", "task", ] as const; + +/** Primary Skywalker: implement surface plus dispatch. */ +export const SKYWALKER_TOOLS = [ + ...IMPLEMENT_TOOLS, + "search_agents", + "task", +] as const; diff --git a/src/agent/directors/types.ts b/src/agent/directors/types.ts index cf51462b5..e002b7927 100644 --- a/src/agent/directors/types.ts +++ b/src/agent/directors/types.ts @@ -25,7 +25,15 @@ export type DirectorId = (typeof DIRECTOR_IDS)[number]; export type TaskIntent = "explore" | "implement" | "plan" | "review" | "general"; /** Static model-role tag for CL-5816 stub resolution (not a full package yet). */ -export type ModelRole = "orchestrator" | "implement" | "explore" | "review" | "plan" | "docs" | "test"; +export type ModelRole = + | "orchestrator" + | "implement" + | "explore" + | "review" + | "plan" + | "docs" + | "test" + | "intern"; export type ToolEnvelope = { /** Tools mounted when present — prefer small allowlists over deny-everything. */ @@ -58,6 +66,7 @@ export type ReportContract = { */ export type DirectorPackage = { readonly id: DirectorId; + readonly name: string; /** Hard primary intent lane — one job. */ readonly primaryIntent: string; /** Explicit out-of-lane work this director must refuse or reclassify. */ @@ -65,7 +74,9 @@ export type DirectorPackage = { readonly description: string; /** Opinionated core prompt (prompt-first). */ readonly systemPrompt: string; - /** Optional skills the worker may load dynamically (ordered). */ + /** Skills this director must load with use_skill before other work (ordered). */ + readonly requiredSkills?: readonly string[]; + /** Skills to load with use_skill when they apply (ordered). */ readonly optionalSkills?: readonly string[]; readonly tools?: ToolEnvelope; /** diff --git a/src/agent/prompt-contract.ts b/src/agent/prompt-contract.ts index db259c99c..64ee5ba7b 100644 --- a/src/agent/prompt-contract.ts +++ b/src/agent/prompt-contract.ts @@ -9,7 +9,7 @@ export const CHAT_PROMPT_QUALITY_MARKERS = [ "Tool choice:", "Ask vs proceed:", "Scope and conventions:", - "Product write tools are not mounted on Skywalker", + "You may edit files", "ask_operator only when permission blocks you", "Touch only code required for the task", "load the style and philosophy skills", diff --git a/src/agent/prompts.test.ts b/src/agent/prompts.test.ts index d72f08db3..1d076608b 100644 --- a/src/agent/prompts.test.ts +++ b/src/agent/prompts.test.ts @@ -13,10 +13,6 @@ import { CORE_TOOL_NAMES, CATALOG_TOOL_NAMES } from "./tool-search.js"; const REGISTERED_TOOL_NAMES = new Set([ ...CORE_TOOL_NAMES, ...CATALOG_TOOL_NAMES, - // Product mutation tools mount on workers, not primary CORE/CATALOG ads. - "write_file", - "edit_file", - "delete_file", ]); const REFERENCED_TOOL_NAMES = [ @@ -82,28 +78,28 @@ describe("shared discipline block appears exactly once per built prompt", () => expect(countOccurrences(prompt, "Prompt discipline:")).toBe(1); }); - it("appears exactly once in a worker prompt (default family)", () => { + it("is omitted from a worker prompt (default family)", () => { const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { orchestrator: false, grokAntiThrash: false, }); - expect(countOccurrences(prompt, "Prompt discipline:")).toBe(1); + expect(countOccurrences(prompt, "Prompt discipline:")).toBe(0); }); - it("appears exactly once in a grok worker prompt", () => { + it("is omitted from a grok worker prompt", () => { const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { orchestrator: false, grokAntiThrash: true, }); - expect(countOccurrences(prompt, "Prompt discipline:")).toBe(1); + expect(countOccurrences(prompt, "Prompt discipline:")).toBe(0); }); - it("appears exactly once in an orchestrator sub-agent prompt", () => { + it("is omitted from an orchestrator sub-agent prompt", () => { const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, { orchestrator: true, grokAntiThrash: false, }); - expect(countOccurrences(prompt, "Prompt discipline:")).toBe(1); + expect(countOccurrences(prompt, "Prompt discipline:")).toBe(0); }); }); diff --git a/src/agent/prompts.ts b/src/agent/prompts.ts index 4b17b3d47..0fba61678 100644 --- a/src/agent/prompts.ts +++ b/src/agent/prompts.ts @@ -28,6 +28,8 @@ const defaultChatTools = [ "list_dir", "lsp", "manage_tasks", + "use_skill", + "tool_search", ]; const joinSections = (sections: string[]) => sections.join("\n\n"); @@ -59,14 +61,7 @@ export function buildHarnessFacts( const subAgent = opts.subAgent ?? false; return [ "Harness facts:", - ...(subAgent - ? [ - "- Change files with write_file/edit_file and remove files with delete_file; shell file-writes and deletions are blocked.", - ] - : [ - "- Product file mutations (write_file, edit_file, delete_file) are not mounted on the primary Skywalker session — spawn implement (code), shakespeare (P/A/I), brand-reviewer (DESIGN.md), or bruckheimer (PRODUCT.md) for durable edits.", - "- Shell file-writes and deletions are blocked; never use echo/heredoc/sed/rm as a substitute for product tools.", - ]), + "- Change files with write_file/edit_file and remove files with delete_file; shell file-writes and deletions are blocked.", "- Use the provided tools for file reads/searches instead of shelling out as a substitute.", "- read_file accepts a filesystem path or a tool-output:///{callId} URI from a prior tool result when the harness exposes one; prefer the URI over re-reading huge blobs.", "- run_shell defaults to a 15s timeout; pass timeout for builds, tests, and other long commands.", @@ -108,12 +103,10 @@ export function buildGuidelines(opts: { subAgent?: boolean; sessionMode?: Sessio ...(subAgent ? [] : [ - "- Prefer task(intent=…) / task(agent=…) for product implementation, exploration, review, and docs — that is the primary loop.", + "- Prefer doing small work yourself; chain task(agent=…) when isolation, parallel map, fresh-eyes review, or a named lane pays.", ]), "- read_file for file contents; grep or search_files to locate code; lsp for symbols, types, references, or call flow before opening large files.", - subAgent - ? "- edit_file for targeted changes; write_file for new files or full rewrites; delete_file to remove files — never echo, heredoc, sed, or rm in the shell for those jobs." - : "- Product write tools are not mounted on Skywalker. Spawn implement (or a docs director) for durable file changes; never shell-write (echo/heredoc/sed/rm).", + "- edit_file for targeted changes; write_file for new files or full rewrites; delete_file to remove files — never shell-write (echo/heredoc/sed/rm).", "- run_shell for builds, tests, git, and one-off commands — not for shell find, head-position rg, or recursive grep -r (OOM risk), cat, or messaging the user.", ...(subAgent ? [] @@ -158,16 +151,12 @@ export function buildGuidelines(opts: { subAgent?: boolean; sessionMode?: Sessio // appended exactly once per built prompt. Prohibition form throughout: these // are the failure modes observed across shipped agents (OpenCode, Codex CLI, // Gemini CLI, Claude Code, Warp, Aider, Cline), not general advice. -export function buildPromptDisciplineBlock(opts: { subAgent?: boolean } = {}): string { - const subAgent = opts.subAgent ?? false; - const toolsOverShell = subAgent - ? "- Never use run_shell to read, edit, or write files — use read_file, edit_file, write_file; cat/head/tail, sed/awk/perl -i, and heredoc/echo redirection are prohibited substitutes." - : "- Never use run_shell to read, edit, or write files — use read_file for reads; durable product edits go through implement/docs directors (write tools are not mounted on Skywalker); cat/head/tail, sed/awk/perl -i, and heredoc/echo redirection are prohibited substitutes."; +export function buildPromptDisciplineBlock(_opts: { subAgent?: boolean } = {}): string { return [ "Prompt discipline:", "", "Tools over shell:", - toolsOverShell, + "- Never use run_shell to read, edit, or write files — use read_file, edit_file, write_file; cat/head/tail, sed/awk/perl -i, and heredoc/echo redirection are prohibited substitutes.", "- Never use echo or shell output to talk to the user — that is what your reply is for.", "", "Environment:", @@ -328,21 +317,23 @@ export function buildChatSystemPrompt( // call `task` (no recursion past depth 1). An orchestrator profile is the // documented exception — its purpose IS to fan work out to other agents — // so the appendix grants permission and links the syntax. +export function buildWorkerHarness(): string { + return [ + "Harness facts:", + "- Mounted tools are listed below. Load more with tool_search. Load a skill body with use_skill before work it covers.", + "- Shell file-writes and deletions are blocked.", + "- You share the parent session's permission gate. You cannot ask the operator — put gaps under Blockers.", + "- Turn budget is real. When the job is done, stop tooling and emit the report envelope.", + "- manage_tasks is yours alone if you use it; it is not shared with the parent.", + ].join("\n"); +} + export function buildSubAgentAppendix(opts: { orchestrator?: boolean } = {}): string { - // Workers must not be told both "spawn with task" and "do not call task". - // Orchestrators get the spawn instruction; everyone else gets the no-recursion - // rule only. const recursionRule = opts.orchestrator === true - ? "- You are an orchestrator: you MAY call `task` to spawn other sub-agents (e.g. task(agent=\"greybeard\", prompt=\"...\")). This is an explicit exception to the no-recursion rule that applies to workers — use it to delegate specialist work, then synthesize their reports into your own. Prefer search_agents before naming a specialist. `task` spawns an agent; it is not a checklist item (use manage_tasks for your own checklist)." - : `- Only the primary ${PRODUCT_NAME} session (or an orchestrator profile) may call \`task\` to spawn sub-agents. You are a worker: return a concrete report to the caller instead of spawning further agents. Use manage_tasks for your own work checklist if the job is multi-step.`; - return [ - `## ${PRODUCT_NAME} notes`, - "", - recursionRule, - `- Tools use ${PRODUCT_NAME} names: read_file, write_file, edit_file, run_shell, search_files, grep, list_dir, lsp, manage_tasks.`, - "- Upstream `mode: primary` is not encoded — every profile here is a spawnable sub-agent definition.", - ].join("\n"); + ? `- You are an orchestrator: you MAY call \`task\` to chain specialists (e.g. task(agent="explore", prompt="...")). Stay inside your spawn allowlist. Prefer search_agents before naming a specialist. \`task\` spawns an agent; manage_tasks is your checklist.` + : `- Only the primary ${PRODUCT_NAME} session (or an orchestrator profile) may call \`task\` to spawn sub-agents. You are a worker: return a concrete report instead of spawning.`; + return [`## ${PRODUCT_NAME} notes`, "", recursionRule].join("\n"); } // Final-reply envelope the parent can parse. Free-form prose is allowed inside @@ -388,37 +379,34 @@ export function buildGrokLeafAntiThrashNote(): string { export function buildSubAgentSystemPrompt( extensions?: string[], env?: EnvironmentInfo, - baseOverride?: string, + identity?: string, opts: { orchestrator?: boolean; toolNames?: readonly string[]; /** When true, append the tiny Grok/xAI finish-bias note (provider residual). */ grokAntiThrash?: boolean; + skills?: readonly SkillSummary[]; } = {}, ): string { - const base = - baseOverride !== undefined && baseOverride.trim().length > 0 - ? baseOverride.trim() - : joinSections([ - `You are a sub-agent — a short-lived child agent dispatched by ${PRODUCT_NAME} to carry out one self-contained job autonomously. You have the full file, search, and shell toolset under the same permission policy as the parent session (saved grants and auto mode when eligible; operator approval otherwise). Finish the job and report back. Your manage_tasks checklist (if you use it) is yours alone; it is not shared with the parent.`, - buildHarnessFacts({ dynamicTools: false, subAgent: true }), - buildGuidelines({ subAgent: true }), - buildPromptDisciplineBlock({ subAgent: true }), - buildSubAgentReportContract(), - ]); + const who = + identity !== undefined && identity.trim().length > 0 + ? identity.trim() + : `You are a worker — a short-lived agent dispatched by ${PRODUCT_NAME} for one job.`; const toolListForPrompt = opts.toolNames && opts.toolNames.length > 0 ? opts.toolNames : defaultChatTools; - const sections = [base, buildAvailableTools(toolListForPrompt), contextSection(env)]; + const sections = [who]; if (extensions !== undefined && extensions.length > 0) { sections.push(...extensions); } + sections.push(buildWorkerHarness()); + if (opts.skills !== undefined && opts.skills.length > 0) { + sections.push(buildSkillsSection(opts.skills)); + } + sections.push(buildAvailableTools(toolListForPrompt), contextSection(env)); if (opts.grokAntiThrash === true) { sections.push(buildGrokLeafAntiThrashNote()); } - // Always-last: the Corbits Code translation notes apply to every dispatched - // agent, regardless of whether its definition came from a JS plugin or a - // corbitsdev-format markdown file. The orchestrator flag rewrites the - // recursion rule for profiles whose purpose is to dispatch other agents. + sections.push(buildSubAgentReportContract()); sections.push(buildSubAgentAppendix(opts)); return joinSections(sections); } diff --git a/src/agent/tool-search.test.ts b/src/agent/tool-search.test.ts index 53d492dc3..a6161edfc 100644 --- a/src/agent/tool-search.test.ts +++ b/src/agent/tool-search.test.ts @@ -9,7 +9,6 @@ import { coreToolNamesForSessionMode, CORE_TOOL_NAMES, CATALOG_TOOL_NAMES, - PRIMARY_DENIED_PRODUCT_TOOLS, type ToolAvailability, } from "./tool-search.js"; @@ -83,12 +82,11 @@ describe("createToolIndex", () => { expect(coreToolNamesForSessionMode("orchestrator", FULL_AVAILABILITY)).not.toContain("present"); }); - test("primary CORE and CATALOG omit product mutation tools", () => { + test("primary CORE advertises product mutation tools", () => { for (const name of ["write_file", "edit_file", "delete_file"] as const) { - expect(CORE_TOOL_NAMES).not.toContain(name); + expect(CORE_TOOL_NAMES).toContain(name); expect(CATALOG_TOOL_NAMES).not.toContain(name); } - expect(PRIMARY_DENIED_PRODUCT_TOOLS).toEqual(["write_file", "edit_file", "delete_file"]); }); test("catalog advertises web_fetch and web_search so URL work needs no tool_search", () => { @@ -193,8 +191,7 @@ describe("advertisedTools", () => { const names = advertisedTools(registry).map((d) => d.name); expect(names).toContain("read_file"); expect(names).toContain("grep"); - // write_file is not in primary CATALOG — product mutations are leaf-only. - expect(names).not.toContain("write_file"); + expect(names).toContain("write_file"); expect(names).not.toContain("mcp__linear__create_issue"); }); diff --git a/src/agent/tool-search.ts b/src/agent/tool-search.ts index b25486811..75acf1ed2 100644 --- a/src/agent/tool-search.ts +++ b/src/agent/tool-search.ts @@ -16,11 +16,11 @@ import { sessionModeEnablesSubAgents } from "../config/session-mode.js"; // dispatchable — the model finds it via tool_search when a session actually // needs it. // -// Product mutation tools (write_file / edit_file / delete_file) are intentionally -// absent from the primary Skywalker core/catalog sets — they mount only on leaf -// directors that need them (implement, shakespeare, …). See PRIMARY_DENIED_PRODUCT_TOOLS. export const CORE_TOOL_NAMES: readonly string[] = [ "read_file", + "write_file", + "edit_file", + "delete_file", "lsp", "run_shell", "ask_operator", @@ -35,13 +35,6 @@ export const CORE_TOOL_NAMES: readonly string[] = [ "task", ]; -/** Product mutation tools denied on the primary Skywalker session (structural). */ -export const PRIMARY_DENIED_PRODUCT_TOOLS: readonly string[] = [ - "write_file", - "edit_file", - "delete_file", -]; - const ORCHESTRATOR_ONLY_TOOL_NAMES: readonly string[] = ["search_agents", "task"]; // Session-start facts that gate a core tool's advertisement. Each must be @@ -77,11 +70,9 @@ export function advertisedToolNamesForSessionMode( // Built-in file/search/web tools advertised alongside the core set. They carry full // schemas on the wire so the model can call them directly; MCP tools are not // listed at all — they are discovered blind via tool_search. -// write_file is intentionally omitted: primary Skywalker does not mutate product -// files; implement/docs leaves mount write tools via their own toolsets. // web_fetch / web_search are catalog (not deferred): URL reads and search are // first-class primary work; requiring tool_search before web_fetch caused -// thrash on web-bait and contradicted the skywalker "already mounted" rule. +// thrash on web-bait. export const CATALOG_TOOL_NAMES: readonly string[] = [ "search_files", "grep", diff --git a/src/agent/tools.ts b/src/agent/tools.ts index aa901b71c..206341a3c 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -32,7 +32,7 @@ import { import type { ToolWatchdogConfig } from "../tui/tool-execution-watchdog.js"; import type { SessionMode } from "../config/session-mode.js"; import { sessionModeEnablesSubAgents } from "../config/session-mode.js"; -import { advertisedToolNamesForSessionMode, PRIMARY_DENIED_PRODUCT_TOOLS, type ToolAvailability } from "./tool-search.js"; +import { advertisedToolNamesForSessionMode, type ToolAvailability } from "./tool-search.js"; import type { ProviderCatalogEntry } from "../config/index.js"; import type { AgentProfile } from "./profiles.js"; import { @@ -247,6 +247,7 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise 0 ? { skillDirs } : {}), }), ...(args.subAgent.profiles !== undefined ? [ @@ -341,13 +342,7 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise !primaryDenied.has(tool.definition.name)); - - const dynamicRunner = createDynamicToolRunner(primaryTools, toolWatchdog); + const dynamicRunner = createDynamicToolRunner(baseTools, toolWatchdog); runnerRef = dynamicRunner; const connectedClients: MCPClient[] = []; diff --git a/src/prompts.test.ts b/src/prompts.test.ts index 4864e0a8b..9dd571b09 100644 --- a/src/prompts.test.ts +++ b/src/prompts.test.ts @@ -39,7 +39,8 @@ test("agent identity is Skywalker orchestrator", () => { expect(orchestrator).toContain("Corbits Code"); expect(orchestrator).toContain("When asked your name, answer: Skywalker"); expect(orchestrator).toContain("PRIMARY INTENT"); - expect(orchestrator).toContain("Delegate"); + expect(orchestrator).toContain("You may edit files"); + expect(orchestrator).toContain("You may chain agents"); expect(orchestrator).toContain("Match operator tone"); // Mode arg is ignored — product is orchestrator-only (CL-5814). expect(buildChatRole()).toContain("You are Skywalker"); @@ -47,8 +48,9 @@ test("agent identity is Skywalker orchestrator", () => { test("harness facts state only the non-derivable tool and safety rules", () => { const facts = buildHarnessFacts(); - expect(facts).toContain("write_file, edit_file, delete_file"); - expect(facts).toContain("not mounted on the primary Skywalker session"); + expect(facts).toContain("write_file/edit_file"); + expect(facts).toContain("delete_file"); + expect(facts).not.toContain("not mounted on the primary Skywalker session"); expect(facts).toContain("blocked"); expect(facts).toContain("15s timeout"); expect(facts).toContain("find, rg, and grep -r"); @@ -254,12 +256,12 @@ test("buildAvailableTools lists exactly the tools it is given", () => { test("sub-agent prompt carries the report-back contract and harness facts", () => { const prompt = buildSubAgentSystemPrompt(); - expect(prompt).toContain("short-lived child agent dispatched by Corbits Code"); + expect(prompt).toContain("short-lived agent dispatched by Corbits Code"); expect(prompt).toContain("Reporting back:"); expect(prompt).toContain("only thing returned to the parent"); - expect(prompt).toContain("Change files with write_file/edit_file"); - expect(prompt).toContain("remove files with delete_file"); expect(prompt).toContain("parent session's permission gate"); + expect(prompt).toContain("tool_search"); + expect(prompt).toContain("use_skill"); expect(prompt).not.toContain("without asking for approval"); expect(prompt).not.toContain("ask_operator"); }); @@ -273,10 +275,11 @@ test("sub-agent report contract treats Success criteria as completion gate", () expect(contract).toContain("Intent / Do not"); }); -test("sub-agent prompt does not advertise tool_search (it gets the full toolset)", () => { +test("sub-agent prompt advertises tool_search and use_skill", () => { const prompt = buildSubAgentSystemPrompt(); - expect(prompt).not.toContain("tool_search"); - expect(prompt).toContain("your full toolset"); + expect(prompt).toContain("tool_search"); + expect(prompt).toContain("use_skill"); + expect(prompt).not.toContain("full file, search, and shell toolset"); }); // Pins the appendix-last invariant for JS-plugin agents: regardless of how the @@ -286,12 +289,11 @@ test("sub-agent prompt does not advertise tool_search (it gets the full toolset) // JS-plugin-only path that bypasses the data-only loader still gets them. test("sub-agent prompt always appends Corbits Code notes, even with a JS-plugin-style systemPromptRole", () => { const role = "You are a JS-plugin scout. Map the call graph and report."; - const prompt = buildSubAgentSystemPrompt([role]); + const prompt = buildSubAgentSystemPrompt(undefined, undefined, role); expect(prompt).toContain(role); expect(prompt).toContain("## Corbits Code notes"); - // Workers get the no-recursion rule, not the spawn syntax. expect(prompt).toContain("You are a worker"); - // Agent voice leads; translation notes are the last section. + expect(prompt.indexOf(role)).toBe(0); expect(prompt.indexOf(role)).toBeLessThan(prompt.indexOf("## Corbits Code notes")); }); @@ -326,7 +328,7 @@ test("sub-agent prompt requires structured report envelope and stick-to-brief", expect(prompt).toContain("## Blockers"); expect(prompt).toContain("## Paths"); expect(prompt).toContain("Stick to the dispatch brief"); - expect(prompt).toContain("manage_tasks checklist"); + expect(prompt).toContain("manage_tasks is yours alone"); }); test("default sub-agent prompt omits Grok anti-thrash residual", () => { diff --git a/src/subagent/run.ts b/src/subagent/run.ts index 59426369c..47a0e318e 100644 --- a/src/subagent/run.ts +++ b/src/subagent/run.ts @@ -38,6 +38,9 @@ import { buildCorePosixToolPlugins } from "../agent/posix-tool-plugins.js"; import { createCompositeBlobReader } from "../agent/lazy-blob-reader.js"; import { buildSubAgentSystemPrompt } from "../agent/prompts.js"; +import { createUseSkillTool } from "../agent/use-skill.js"; +import { createToolIndex, createToolSearchTool } from "../agent/tool-search.js"; +import { discoverSkills } from "../extensions/skills.js"; import { shouldApplyGrokAntiThrash } from "./provider-family.js"; import { resolveModelFamilyPolicy } from "../agent/model-family-policy.js"; import { normalizeToolDefinitionsForProvider } from "../agent/tool-schema-normalize.js"; @@ -164,10 +167,13 @@ export function coreSubAgentWebTools(): AgentTool[] { function applyCapabilityFilter(tools: AgentTool[], capabilities: CapabilityFilter): AgentTool[] { const nameSet = new Set(capabilities.tools); - if (capabilities.mode === "exclude") { - return tools.filter((t) => !nameSet.has(t.definition.name)); - } - return tools.filter((t) => nameSet.has(t.definition.name)); + const keep = (name: string): boolean => { + // MCP tools stay dispatchable so tool_search can load them; the envelope + // only constrains the posix/web surface advertised up front. + if (name.startsWith("mcp__")) return true; + return capabilities.mode === "exclude" ? !nameSet.has(name) : nameSet.has(name); + }; + return tools.filter((t) => keep(t.definition.name)); } export type SubAgentRunController = { @@ -314,6 +320,22 @@ export async function runSubAgent(params: RunSubAgentParams): Promise { return "Tasks updated."; }, }), + createUseSkillTool(params.cwd, params.skillDirs ?? []), + ]; + const advertised = new Set( + tools.map((t) => t.definition.name).filter((name) => !name.startsWith("mcp__")), + ); + const toolIndex = createToolIndex( + () => tools.map((t) => t.definition), + [...advertised, "tool_search"], + ); + tools = [ + ...tools, + createToolSearchTool({ + search: (query) => toolIndex.search(query), + lookup: (name) => tools.find((t) => t.definition.name === name)?.definition, + promote: () => undefined, + }), ]; // Orchestrators need task + search_agents installed, not just mentioned in @@ -354,6 +376,7 @@ export async function runSubAgent(params: RunSubAgentParams): Promise { ...(nd.parentSessionId !== undefined ? { parentSessionId: nd.parentSessionId } : {}), ...(nd.useWorktree !== undefined ? { useWorktree: nd.useWorktree } : {}), ...(nd.spawnAllowlist !== undefined ? { spawnAllowlist: nd.spawnAllowlist } : {}), + ...(nd.skillDirs !== undefined ? { skillDirs: nd.skillDirs } : {}), }), ...(nd.profiles !== undefined ? [ @@ -367,12 +390,14 @@ export async function runSubAgent(params: RunSubAgentParams): Promise { } const environment = await gatherEnvironment(params.cwd); - const extensions = - params.systemPromptRole !== undefined ? [params.systemPromptRole] : undefined; - const toolNames = tools.map((t) => t.definition.name); - const systemPrompt = buildSubAgentSystemPrompt(extensions, environment, undefined, { + const skills = await discoverSkills(params.cwd, params.skillDirs ?? []); + const toolNames = tools + .map((t) => t.definition.name) + .filter((name) => !name.startsWith("mcp__")); + const systemPrompt = buildSubAgentSystemPrompt(undefined, environment, params.systemPromptRole, { orchestrator: params.orchestrator === true, toolNames, + ...(skills.length > 0 ? { skills } : {}), grokAntiThrash: shouldApplyGrokAntiThrash({ providerName: params.provider.providerName, model: params.provider.model, diff --git a/src/subagent/task-tool.ts b/src/subagent/task-tool.ts index 24d5e2d3f..da33a25fe 100644 --- a/src/subagent/task-tool.ts +++ b/src/subagent/task-tool.ts @@ -563,6 +563,7 @@ export function createTaskTool(deps: TaskToolDeps): AgentTool { ...(deps.shellEnv !== undefined ? { shellEnv: deps.shellEnv } : {}), ...(deps.extraToolPlugins !== undefined ? { extraToolPlugins: deps.extraToolPlugins } : {}), ...(deps.getBlobReader !== undefined ? { getBlobReader: deps.getBlobReader } : {}), + ...(deps.skillDirs !== undefined ? { skillDirs: deps.skillDirs } : {}), }; const nestedDispatch: NestedDispatchDeps | undefined = orchestrator ? { diff --git a/src/subagent/types.ts b/src/subagent/types.ts index 91871c1ad..b4953d1a8 100644 --- a/src/subagent/types.ts +++ b/src/subagent/types.ts @@ -45,6 +45,8 @@ export type SubAgentSandboxDeps = { getBlobReader?: () => BlobReader | undefined; /** Project settings.env, merged into the sub-agent's run_shell spawn environment. */ shellEnv?: Record; + /** Plugin/project skill directories so workers can load bodies via use_skill. */ + skillDirs?: string[]; }; export type NestedDispatchDeps = SubAgentSandboxDeps & { @@ -98,6 +100,8 @@ export type RunSubAgentParams = { onProgress?: (info: { description: string; toolName: string }) => void; capabilities?: CapabilityFilter; systemPromptRole?: string; + /** Plugin/project skill directories so this worker can call use_skill. */ + skillDirs?: string[]; /** * Director authz write-path allowlist. Passed into sub-agent identity so the * permission gate can deny out-of-lane writes (not prompt policy). diff --git a/tests/unit/subagent.test.ts b/tests/unit/subagent.test.ts index 4db6644b8..d3ac62adb 100644 --- a/tests/unit/subagent.test.ts +++ b/tests/unit/subagent.test.ts @@ -143,10 +143,8 @@ test("handler reports runner failures without throwing", async () => { test("sub-agent prompt is autonomous and forbids recursion for workers", () => { const prompt = buildSubAgentSystemPrompt(); - expect(prompt).toContain("sub-agent"); - expect(prompt).toContain("permission policy as the parent session"); + expect(prompt).toContain("short-lived agent"); expect(prompt).toContain("parent session's permission gate"); - // Workers must not be invited to spawn further agents. expect(prompt).toContain("You are a worker"); expect(prompt).not.toContain("MAY call `task`"); }); diff --git a/tests/unit/tui/agent-tools.test.ts b/tests/unit/tui/agent-tools.test.ts index 59d576a21..852c2f7f1 100644 --- a/tests/unit/tui/agent-tools.test.ts +++ b/tests/unit/tui/agent-tools.test.ts @@ -191,10 +191,7 @@ test("dynamicRunner contains posix tool names plus ask_operator", async () => { const names = toolset.dynamicRunner.currentDefinitions().map((d) => d.name); expect(names).toContain("read_file"); expect(names).toContain("ask_operator"); - // Primary Skywalker never mounts product mutation tools. - expect(names).not.toContain("write_file"); - expect(names).not.toContain("edit_file"); - expect(names).not.toContain("delete_file"); + expect(names).toContain("write_file"); }); test("onOperatorGate callback is invoked when the operator tool handler is called", async () => {