@worca/app 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +22 -9
  2. package/agents/clarify.meta.json +4 -4
  3. package/agents/decomposer.meta.json +5 -5
  4. package/agents/implementer.meta.json +15 -5
  5. package/agents/manualTestsChecklist.meta.json +5 -4
  6. package/agents/manualWebUiTesting.meta.json +9 -4
  7. package/agents/planReviewer.meta.json +12 -4
  8. package/agents/planner.meta.json +12 -5
  9. package/agents/refiner.meta.json +15 -4
  10. package/agents/reviewer.meta.json +14 -4
  11. package/agents/worca-cc-clarify.md +7 -0
  12. package/agents/worca-cc-code-reviewer.md +11 -6
  13. package/agents/worca-cc-decomposer.md +7 -0
  14. package/agents/worca-cc-implementer.md +9 -0
  15. package/agents/worca-cc-manual-tests-checklist.md +8 -5
  16. package/agents/worca-cc-manual-web-ui-testing.md +10 -6
  17. package/agents/worca-cc-plan-refiner.md +11 -6
  18. package/agents/worca-cc-plan-reviewer.md +10 -7
  19. package/agents/worca-cc-planner.md +9 -0
  20. package/agents/worca-cc-workspace-reviewer.md +11 -4
  21. package/agents/worca-cc-workspace-scanner.md +8 -4
  22. package/agents/workspaceReviewer.meta.json +15 -4
  23. package/agents/workspaceScanner.meta.json +5 -4
  24. package/package.json +8 -2
  25. package/skills/worca/SKILL.md +5 -5
  26. package/src/cli/render.mjs +148 -0
  27. package/src/cli/worca-cc.mjs +319 -45
  28. package/src/core/agent-gen.mjs +69 -31
  29. package/src/core/agent-registry.mjs +124 -144
  30. package/src/core/agent-store.mjs +164 -4
  31. package/src/core/artifacts.mjs +189 -21
  32. package/src/core/ask/catalog.mjs +111 -0
  33. package/src/core/ask/comment-deps.mjs +55 -0
  34. package/src/core/ask/events.mjs +506 -0
  35. package/src/core/ask/follow.mjs +107 -0
  36. package/src/core/ask/git-allowlist.mjs +226 -0
  37. package/src/core/ask/limits.mjs +54 -0
  38. package/src/core/ask/mcp-stdio.mjs +135 -0
  39. package/src/core/ask/models.mjs +125 -0
  40. package/src/core/ask/prompt.mjs +261 -0
  41. package/src/core/ask/proposal.mjs +170 -0
  42. package/src/core/ask/redact.mjs +30 -0
  43. package/src/core/ask/spawn.mjs +153 -0
  44. package/src/core/ask/store.mjs +360 -0
  45. package/src/core/ask/tool-deps.mjs +63 -0
  46. package/src/core/ask/tools.mjs +848 -0
  47. package/src/core/ask/turn.mjs +416 -0
  48. package/src/core/ask/worktree-deps.mjs +27 -0
  49. package/src/core/ask/worktrees.mjs +285 -0
  50. package/src/core/chat/command-router.mjs +20 -3
  51. package/src/core/claude-runner.mjs +434 -57
  52. package/src/core/config.mjs +264 -41
  53. package/src/core/cost-budget.mjs +29 -2
  54. package/src/core/db.mjs +684 -47
  55. package/src/core/diff-anchor.mjs +213 -0
  56. package/src/core/diff-comments.mjs +273 -0
  57. package/src/core/engine-select.mjs +32 -0
  58. package/src/core/git-info.mjs +49 -10
  59. package/src/core/graph/builtin-workflows.mjs +51 -0
  60. package/src/core/graph/executor.mjs +894 -0
  61. package/src/core/graph/registry-ports.mjs +12 -0
  62. package/src/core/graph/scheduler.mjs +1065 -0
  63. package/src/core/graph/seed-templates.mjs +318 -0
  64. package/src/core/model-env.mjs +112 -8
  65. package/src/core/model-test.mjs +79 -0
  66. package/src/core/orchestrator.mjs +902 -4098
  67. package/src/core/overview-agent.mjs +15 -3
  68. package/src/core/phases.mjs +208 -537
  69. package/src/core/pipeline-delete.mjs +13 -2
  70. package/src/core/plugin-api.mjs +8 -3
  71. package/src/core/plugin-config.mjs +178 -28
  72. package/src/core/plugin-inventory.mjs +6 -2
  73. package/src/core/plugin-manifest.mjs +199 -11
  74. package/src/core/plugin-models.mjs +1 -0
  75. package/src/core/plugin-repo.mjs +16 -4
  76. package/src/core/plugin-shim-child.mjs +9 -3
  77. package/src/core/plugin-shim.mjs +77 -14
  78. package/src/core/plugin-store.mjs +236 -29
  79. package/src/core/plugin-workflows.mjs +90 -41
  80. package/src/core/preflight.mjs +135 -3
  81. package/src/core/projects.mjs +7 -5
  82. package/src/core/protocol.mjs +8 -35
  83. package/src/core/recoverable-error.mjs +1 -1
  84. package/src/core/run-harness.mjs +3585 -0
  85. package/src/core/run-manifest.mjs +5 -1
  86. package/src/core/settings.mjs +109 -13
  87. package/src/core/skills.mjs +10 -3
  88. package/src/core/source-bindings.mjs +175 -0
  89. package/src/core/sources.mjs +87 -25
  90. package/src/core/stats.mjs +25 -6
  91. package/src/core/title.mjs +51 -4
  92. package/src/core/workflows.mjs +358 -259
  93. package/src/core/workspace-scan.mjs +4 -0
  94. package/src/core/worktree.mjs +98 -7
  95. package/src/shared/graph/agent-meta.mjs +278 -0
  96. package/src/shared/graph/constants.mjs +105 -0
  97. package/src/shared/graph/geometry.mjs +157 -0
  98. package/src/shared/graph/layout.mjs +134 -0
  99. package/src/shared/graph/loops.mjs +130 -0
  100. package/src/shared/graph/manifest.mjs +257 -0
  101. package/src/shared/graph/ports.mjs +153 -0
  102. package/src/shared/graph/route.mjs +397 -0
  103. package/src/shared/graph/template.mjs +165 -0
  104. package/src/shared/graph/thumbnail.mjs +67 -0
  105. package/src/shared/graph/validate.mjs +491 -0
  106. package/src/shared/graph/verdict.mjs +41 -0
  107. package/ui/public/app.js +4008 -1670
  108. package/ui/public/ask-markdown.mjs +145 -0
  109. package/ui/public/ask-model.mjs +264 -0
  110. package/ui/public/ask-panel.mjs +1880 -0
  111. package/ui/public/chat-settings-view.mjs +6 -2
  112. package/ui/public/diff-view.mjs +66 -11
  113. package/ui/public/file-tree.mjs +305 -0
  114. package/ui/public/graph/composer.mjs +889 -0
  115. package/ui/public/graph/inspector.mjs +183 -0
  116. package/ui/public/graph/model.mjs +37 -0
  117. package/ui/public/graph/palette.mjs +144 -0
  118. package/ui/public/graph/run-decor.mjs +410 -0
  119. package/ui/public/graph/run-hosts.mjs +201 -0
  120. package/ui/public/graph/save-dialog.mjs +56 -0
  121. package/ui/public/graph/view.mjs +858 -0
  122. package/ui/public/guardrails-view.mjs +4 -2
  123. package/ui/public/hljs-loader.mjs +180 -0
  124. package/ui/public/index.html +269 -265
  125. package/ui/public/log-filter.mjs +22 -4
  126. package/ui/public/log-line.mjs +45 -19
  127. package/ui/public/models-view.mjs +171 -9
  128. package/ui/public/plugins-view.mjs +106 -4
  129. package/ui/public/source-pane.mjs +190 -8
  130. package/ui/public/stats-view.mjs +81 -1
  131. package/ui/public/style.css +1459 -229
  132. package/ui/public/syntax-highlight.mjs +270 -0
  133. package/ui/public/thinking-orb.mjs +110 -0
  134. package/ui/server.mjs +1667 -98
  135. package/src/core/channels.mjs +0 -302
  136. package/src/core/runners.mjs +0 -167
  137. package/src/core/workflow-validator.mjs +0 -185
  138. package/ui/public/composer-core.mjs +0 -211
@@ -15,16 +15,17 @@
15
15
 
16
16
  import { runClaude } from './claude-runner.mjs';
17
17
  import { resolveModelEnv } from './config.mjs';
18
+ import { SUBAGENT_AUTO, SUBAGENT_INHERIT, SUBAGENT_MODELS, effectiveSubagentModel } from './model-env.mjs';
18
19
  import { readClarify, readReview } from './protocol.mjs';
19
20
  import { writeClarify, readClarifyRow } from './artifacts.mjs';
20
- import { renderAttachmentsBlock } from './channels.mjs';
21
+ import { join } from 'node:path';
21
22
 
22
23
  // ── allowedTools per role ──────────────────────────────────────────────────────
23
24
  // `Skill` lets agents invoke project (.claude/skills) and personal (~/.claude/skills)
24
25
  // skills via the Skill tool; without it, headless `claude -p` denies skill calls.
25
- const READ_WRITE_TOOLS = ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'Skill'];
26
+ export const READ_WRITE_TOOLS = ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'Skill'];
26
27
  // Implementer additionally gets MultiEdit for larger, multi-hunk edits.
27
- const IMPLEMENTER_TOOLS = ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Grep', 'Glob', 'Skill'];
28
+ export const IMPLEMENTER_TOOLS = ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Grep', 'Glob', 'Skill'];
28
29
 
29
30
  /**
30
31
  * Effective `--allowedTools` for a node: the role's baseline file/exec tools UNION
@@ -72,6 +73,109 @@ export function ctxFanOut(ctx) {
72
73
  return !!(ctx.node ? ctx.node.fanOut : ctx.fanOut);
73
74
  }
74
75
 
76
+ /**
77
+ * Whether this run's EFFECTIVE model routes to a custom endpoint (an
78
+ * ANTHROPIC_BASE_URL-overriding catalog/plugin entry). Stamped at dispatch
79
+ * time — orchestrator._execCtx on ctx.node, workspace-scan on the node-less
80
+ * ctx — from modelHasBaseUrlRouting(effective model), so this stays pure.
81
+ * A present node wins, mirroring ctxFanOut. Pure + exported for testing.
82
+ */
83
+ export function ctxEndpointRouted(ctx) {
84
+ if (!ctx || typeof ctx !== 'object') return false;
85
+ return !!(ctx.node ? ctx.node.endpointRouted : ctx.endpointRouted);
86
+ }
87
+
88
+ /**
89
+ * The sub-agent model policy in force for this run: one of SUBAGENT_MODEL_VALUES.
90
+ * Read from the NODE only — nothing else carries the setting (the v1 clarify
91
+ * pre-step ctx fallback is gone with its caller). An unset or off-vocabulary
92
+ * value resolves to the auto default: agents choose their children's models BY
93
+ * DEFAULT, and 'inherit' is the stored opt-out.
94
+ *
95
+ * GATED ON FAN-OUT: a node that cannot spawn children has no children to place,
96
+ * so a stale setting on a node whose fan-out was turned off resolves to
97
+ * 'inherit' and emits no prompt block — every non-fan-out prompt keeps today's
98
+ * bytes. Pure + exported for testing.
99
+ */
100
+ export function ctxSubagentModel(ctx) {
101
+ if (!ctxFanOut(ctx)) return SUBAGENT_INHERIT;
102
+ return effectiveSubagentModel(ctx && ctx.node ? ctx.node.subagentModel : undefined);
103
+ }
104
+
105
+ /**
106
+ * The prompt wire of the sub-agent model policy — the ONLY wire. The CLI
107
+ * resolves a child's model as Task-call `model` > the agent definition's own
108
+ * `model:` frontmatter > env default > parent, so an omitted parameter lands
109
+ * wherever the chosen agent definition says: both modes therefore instruct an
110
+ * EXPLICIT `model` on every call, attributed to the operator (the Task tool's
111
+ * own schema tells the model to set `model` only when a user asked for it, so
112
+ * an unattributed hint would correctly be ignored).
113
+ *
114
+ * 'inherit' — and any value that escaped validation — contributes NOTHING, so
115
+ * that prompt keeps the pre-feature bytes. The auto rubric keys on WHO CHECKS
116
+ * THE OUTPUT rather than on apparent difficulty (the agent is sizing a sub-task
117
+ * before anything has been read, and that estimate runs optimistic), and its
118
+ * tiers describe READ-ONLY investigation — the enclosing fan-out block forbids
119
+ * writing children. Pure + exported for testing.
120
+ */
121
+ export function subagentModelDirective(subagentModel) {
122
+ if (subagentModel === SUBAGENT_AUTO) {
123
+ return (
124
+ '### Sub-agent model — YOUR call, per spawn\n\n' +
125
+ 'The operator has asked you to choose each sub-agent\'s model deliberately: pass `model` on ' +
126
+ 'EVERY Task/Agent call — this instruction is that request (the tool\'s usual "only when ' +
127
+ 'explicitly asked" caveat is satisfied here). Never omit the parameter: an agent definition ' +
128
+ 'may pin its own default, so an omitted `model` lands wherever that definition says, not ' +
129
+ 'where you intend. Legal values: `sonnet`, `opus`, `fable`.\n\n' +
130
+ 'Choose on WHO CHECKS THE OUTPUT, never on how small or cheap the sub-task looks:\n' +
131
+ '- `sonnet`: mechanical, bounded investigation whose findings the report itself lets you ' +
132
+ 'verify — grep-and-summarize a known pattern, enumerate usages or call sites, extract or ' +
133
+ 'reformat existing content, confirm what a file plainly states.\n' +
134
+ '- `opus`: investigation needing real codebase judgment you will build on — trace why ' +
135
+ 'something fails, map how a subsystem hangs together, weigh where a change belongs.\n' +
136
+ '- `fable`: analysis whose VERDICT the run depends on and nothing downstream re-checks — a ' +
137
+ 'severity call, a design or plan judgement, an accept/reject recommendation.\n\n' +
138
+ 'When unsure between two tiers, take the lower one only if you will verify the result ' +
139
+ 'yourself.\n\n'
140
+ );
141
+ }
142
+ if (!SUBAGENT_MODELS.includes(subagentModel)) return '';
143
+ return (
144
+ `### Sub-agent model — \`${subagentModel}\`\n\n` +
145
+ `The operator pinned this node's sub-agents to \`${subagentModel}\`: pass ` +
146
+ `\`model: "${subagentModel}"\` on EVERY Task/Agent call — this instruction is that request ` +
147
+ '(the tool\'s usual "only when explicitly asked" caveat is satisfied here). Never omit the ' +
148
+ 'parameter: an agent definition may pin its own default, and an omitted `model` would land ' +
149
+ 'there instead of on the pin. Do not pick any other value; split the work so it suits that ' +
150
+ 'tier.\n\n'
151
+ );
152
+ }
153
+
154
+ /**
155
+ * The sub-agent block for an endpoint-routed node — it REPLACES
156
+ * subagentModelDirective for every stored value (auto, a pin, inherit): the
157
+ * CLI's Task `model` parameter takes only the alias enum, every alias expands
158
+ * to an Anthropic id the custom endpoint does not serve, and the omitted
159
+ * parameter inherits the parent's wire model ONLY through an agent definition
160
+ * that pins no `model:` frontmatter. So the one working policy is: no `model`
161
+ * parameter, unpinned agent types. Pure + exported for testing.
162
+ */
163
+ export function sameEndpointSubagentDirective() {
164
+ return (
165
+ '### Sub-agent model — same endpoint as this node (locked)\n\n' +
166
+ 'This node runs on an operator-configured custom endpoint that serves ONLY this node\'s own ' +
167
+ 'model. Alias models (`sonnet`, `opus`, `fable`, `haiku`) are NOT served there:\n' +
168
+ '- NEVER pass a `model` parameter on any Task/Agent call. An omitted `model` lets the child ' +
169
+ 'run where you run — the only model this endpoint serves. Any alias you pass would fail the ' +
170
+ 'child at spawn.\n' +
171
+ '- Spawn ONLY `subagent_type` values whose definition pins no `model:` frontmatter: ' +
172
+ '`"general-purpose"` is always safe. AVOID `"Explore"` and any project/personal agent whose ' +
173
+ 'definition file sets `model:` — such a child would request the pinned model from this ' +
174
+ 'endpoint and die. When unsure what an agent pins, use `"general-purpose"` with a ' +
175
+ 'task-specific prompt.\n\n'
176
+ );
177
+ }
178
+
75
179
  // ── run-root mode gates for the §5.8 prompt variants ───────────────────────────
76
180
  // EVERY Phase-4 prompt variant is gated on `runRootMode === 'detached'`; the
77
181
  // workspace-specific ones are ADDITIONALLY gated on `isWorkspace` (§6 Phase 4).
@@ -119,16 +223,31 @@ function relRepo(p) {
119
223
  * promised — it is inherited env and remains true. Single mode (both run-root modes)
120
224
  * and legacy workspace runs keep today's byte-identical sentence.
121
225
  */
122
- export function fanOutDirective(fanOut, { omitProjectAgents = false } = {}) {
226
+ export function fanOutDirective(fanOut, { omitProjectAgents = false, subagentModel = '', endpointRouted = false } = {}) {
123
227
  if (!fanOut) return '';
124
- const subagentSentence = omitProjectAgents
125
- ? 'Pick the BEST-FIT `subagent_type`: your personal agents (`~/.claude/agents`) are available by ' +
126
- 'name prefer a purpose-built one when it fits the sub-task, else fall back to ' +
127
- '`"general-purpose"` (or `"Explore"` for pure code search). This run starts at the worca-cc run ' +
128
- 'root, so no member project\'s own agents are discoverable by name.'
129
- : 'Pick the BEST-FIT `subagent_type`: this project\'s own agents (`.claude/agents`) and your personal ' +
130
- 'agents (`~/.claude/agents`) are available by name prefer a purpose-built one when it fits the ' +
131
- 'sub-task, else fall back to `"general-purpose"` (or `"Explore"` for pure code search).';
228
+ // Endpoint-routed: the usual "prefer a purpose-built agent" steering would
229
+ // walk the agent straight into frontmatter-pinned definitions whose model the
230
+ // custom endpoint cannot serve swap the sentence AND the model block.
231
+ const subagentSentence = endpointRouted
232
+ ? 'Use `subagent_type: "general-purpose"` for EVERY spawn unless you have verified (by reading ' +
233
+ 'its definition file) that a purpose-built agent pins no `model:` in its frontmatter — this ' +
234
+ 'node runs on a custom endpoint that serves only its own model (details in the sub-agent ' +
235
+ 'model block below).' +
236
+ // A detached-workspace run keeps its run-root caveat: the routed steer
237
+ // sends the agent checking definition files, and a run-root cwd cannot
238
+ // discover member projects' agents by name in the first place.
239
+ (omitProjectAgents
240
+ ? ' This run starts at the worca-cc run root, so no member project\'s own agents are ' +
241
+ 'discoverable by name.'
242
+ : '')
243
+ : omitProjectAgents
244
+ ? 'Pick the BEST-FIT `subagent_type`: your personal agents (`~/.claude/agents`) are available by ' +
245
+ 'name — prefer a purpose-built one when it fits the sub-task, else fall back to ' +
246
+ '`"general-purpose"` (or `"Explore"` for pure code search). This run starts at the worca-cc run ' +
247
+ 'root, so no member project\'s own agents are discoverable by name.'
248
+ : 'Pick the BEST-FIT `subagent_type`: this project\'s own agents (`.claude/agents`) and your personal ' +
249
+ 'agents (`~/.claude/agents`) are available by name — prefer a purpose-built one when it fits the ' +
250
+ 'sub-task, else fall back to `"general-purpose"` (or `"Explore"` for pure code search).';
132
251
  return (
133
252
  '## Fan-out ENABLED — parallelize your research\n\n' +
134
253
  'The Task/Agent tool is in your tool list this run. For any non-trivial task that spans more ' +
@@ -141,7 +260,8 @@ export function fanOutDirective(fanOut, { omitProjectAgents = false } = {}) {
141
260
  '`~/.claude/skills`) can be invoked via the Skill tool — by you AND by the sub-agents you spawn — ' +
142
261
  'use any that fit (e.g. design, framework-pattern, knowledge-graph) instead of guessing conventions.\n\n' +
143
262
  'Sub-agents are strictly READ-ONLY investigators: YOU write every artifact. Skip fan-out only for a ' +
144
- 'trivial, single-file change.\n\n'
263
+ 'trivial, single-file change.\n\n' +
264
+ (endpointRouted ? sameEndpointSubagentDirective() : subagentModelDirective(subagentModel))
145
265
  );
146
266
  }
147
267
 
@@ -175,12 +295,16 @@ export function workspaceContextBlock(ws) {
175
295
  * every member checkout is INSIDE the shared cwd (`<runRoot>`), so a sub-agent must
176
296
  * not chdir anywhere. Merge order and the anti-recursion clause are identical in
177
297
  * both variants, and the legacy text is byte-identical to today.
298
+ * `endpointRouted` (the same dispatch stamp `fanOutDirective` reads) swaps the
299
+ * explore arm's `Explore` steering for a `general-purpose` investigator, so a
300
+ * routed node is never ordered into a model-pinned agent one paragraph after
301
+ * the same-endpoint block forbade it; legacy/default bytes are unchanged.
178
302
  * @param {'explore'|'task'|'review'} strategy
179
303
  * @param {{projects?:Array<{projectName?:string,projectKey?:string}>}|null|undefined} ws
180
- * @param {{relative?:boolean}} [opts]
304
+ * @param {{relative?:boolean, endpointRouted?:boolean}} [opts]
181
305
  * @returns {string}
182
306
  */
183
- export function workspaceFanOutDirective(strategy, ws, { relative = false } = {}) {
307
+ export function workspaceFanOutDirective(strategy, ws, { relative = false, endpointRouted = false } = {}) {
184
308
  if (!ws) return '';
185
309
  const ANTI_RECURSION =
186
310
  'Sub-agents are strictly single-level: a sub-agent MUST NOT re-fan-out ' +
@@ -189,7 +313,11 @@ export function workspaceFanOutDirective(strategy, ws, { relative = false } = {}
189
313
  if (strategy === 'explore') {
190
314
  return (
191
315
  '## Workspace fan-out — explore across member projects\n\n' +
192
- 'Dispatch ONE read-only Explore sub-agent per member project (cap 8) to survey ' +
316
+ // Endpoint-routed: `Explore` pins a model the custom endpoint cannot serve
317
+ // the same swap the generic fan-out block makes (sameEndpointSubagentDirective).
318
+ (endpointRouted
319
+ ? 'Dispatch ONE read-only `general-purpose` investigator per member project (cap 8) to survey '
320
+ : 'Dispatch ONE read-only Explore sub-agent per member project (cap 8) to survey ') +
193
321
  (relative
194
322
  ? 'its checkout at `./repos/<projectKey>` inside the shared cwd (modules, public ' +
195
323
  'API, deps) — read files there directly and use `git -C repos/<projectKey> …` ' +
@@ -236,51 +364,6 @@ export function workspaceFanOutDirective(strategy, ws, { relative = false } = {}
236
364
  return '';
237
365
  }
238
366
 
239
- // ── inline fallbacks (used only when agents/*.md is missing/empty) ──────────────
240
- const FALLBACK_PROMPTS = {
241
- clarify:
242
- 'You are the Clarify agent. Before a software task is planned you surface the decisions that ' +
243
- 'materially change the plan and cannot be resolved from the task text or the codebase — ' +
244
- 'including things downstream agents would otherwise silently assume. For each, write a ' +
245
- 'conceptual question offering 2 to 4 options plus a free-text field. Ask up to 8 questions, ' +
246
- 'but never pad. Output a JSON file (path given in the task) shaped as ' +
247
- '{ "questions": [ { "id", "question", "options": [ ... ], "allowFreeText": true } ] }. ' +
248
- 'If you genuinely have no open questions, write { "questions": [] }. You never write a plan.',
249
- 'planner-plan':
250
- 'You are the Planner. Write a thorough implementation plan to the markdown path given in ' +
251
- 'the task. The plan MUST include concrete code snippets for the features and MUST end with ' +
252
- 'a "## Clarifications (Q&A)" section listing what was asked and how the user answered. ' +
253
- 'When done, hand off naming the plan file location.',
254
- refiner:
255
- 'You are the Plan Refiner. Critically review the given plan (including its code snippets), ' +
256
- 'write a refined version to the output path, and emit a review JSON ' +
257
- '({ "issues": [ { "severity", "title", "detail", "location" } ], "summary" }) using ' +
258
- 'severities critical|major|minor|suggestion. Only critical/major are blocking.',
259
- implementer:
260
- 'You are the Implementer. Follow the latest plan with NO deviation, using TDD ' +
261
- '(red-green-refactor). Deviate only if something does not work at all. In fix mode, address ' +
262
- 'every critical/major issue in the referenced review.',
263
- reviewer:
264
- 'You are the Code Reviewer. Review the git diff of what was implemented against the plan. ' +
265
- 'Write a human-readable review markdown AND a review JSON ' +
266
- '({ "issues": [ { "severity", "title", "detail", "location" } ], "summary" }). ' +
267
- 'Use severities critical|major|minor|suggestion; only critical/major block.',
268
- 'manual-tests-checklist':
269
- 'You are the Manual Tests author. Read the plan and the implemented diff, then write a ' +
270
- 'markdown checklist of manual test cases (each a `- [ ]` line with steps + expected result) ' +
271
- 'to the path given in the task.',
272
- 'manual-web-ui-testing':
273
- 'You are the Manual Web UI Tester. Run each case in the manual checklist against the live ' +
274
- 'web UI using the Playwright tools, then write a result markdown AND a review JSON ' +
275
- '({ "issues": [ { "severity", "title", "detail", "location" } ], "summary" }). Use severities ' +
276
- 'critical|major|minor|suggestion; a failing case is at least major.',
277
- 'plan-review':
278
- 'You are the Plan Reviewer. Review the implementation PLAN (its structure, correctness, ' +
279
- 'completeness, feasibility, and code snippets) against the original request and the real ' +
280
- 'codebase. Do NOT rewrite the plan. Write a human-readable review markdown AND a review JSON ' +
281
- '({ "issues": [ { "severity", "title", "detail", "location" } ], "summary" }) using severities ' +
282
- 'critical|major|minor|suggestion; only critical/major block (the planner then revises).',
283
- };
284
367
 
285
368
  /**
286
369
  * Build the full appended system prompt: toolInstruction first (if any), then — on
@@ -297,7 +380,10 @@ export function buildSystemPrompt(toolInstruction, agentBody, role, workspace) {
297
380
  const ws = workspaceContextBlock(workspace); // '' when not a workspace run
298
381
  if (ws) parts.push(ws);
299
382
  const body = (agentBody || '').trim();
300
- parts.push(body || FALLBACK_PROMPTS[role] || '');
383
+ // The agent's .md body IS the contract (spec §1: the engine is generic). The v1
384
+ // per-role FALLBACK_PROMPTS table died with the v1 engine; a missing body now
385
+ // yields a body-less system prompt rather than a hard-coded role script.
386
+ parts.push(body || '');
301
387
  return parts.filter(Boolean).join('\n\n');
302
388
  }
303
389
 
@@ -318,7 +404,7 @@ export function resolveAgentBody(ctx, key) {
318
404
  }
319
405
 
320
406
  /** Render the MOCK marker block appended to every task prompt. */
321
- function mockMarkers(fields) {
407
+ export function mockMarkers(fields) {
322
408
  const lines = [];
323
409
  for (const [key, val] of Object.entries(fields)) {
324
410
  if (val === undefined || val === null || val === '') continue;
@@ -363,13 +449,14 @@ export function questionsPromptBlock(ctx) {
363
449
  return (
364
450
  '\n\n' + answered +
365
451
  '## Asking the user (enabled)\n\n' +
366
- 'If a decision genuinely blocks correct work and cannot be resolved from the task, the ' +
367
- 'inputs, or the codebase:\n' +
452
+ 'If a decision materially shapes the outcome and you cannot resolve it from the task, ' +
453
+ 'the inputs, or the codebase — including anything material you are about to silently ' +
454
+ 'assume:\n' +
368
455
  '1. Write {"questions":[{"id","question","options":[2-4 strings],"allowFreeText":true}]} ' +
369
456
  `(max 8 questions) to: ${ctx.questionsFile}\n` +
370
457
  '2. STOP immediately — do no further work. You will be resumed with the answers.\n' +
371
- 'Prefer reasonable assumptions for minor choices; never pad, and never re-ask an ' +
372
- 'answered question.\n\n' +
458
+ 'Assume freely on minor choices; on material ones, ask instead of assuming. Never pad, ' +
459
+ 'and never re-ask an answered question.\n\n' +
373
460
  mock
374
461
  );
375
462
  }
@@ -394,7 +481,7 @@ export function workspaceWriteTargetsFor(ctx) {
394
481
  }
395
482
 
396
483
  /** Map the orchestrator's claudeOpts into runClaude options shared by every role. */
397
- function runOpts(ctx, { role, prompt, systemPrompt, allowedTools }) {
484
+ export function runOpts(ctx, { role, prompt, systemPrompt, allowedTools }) {
398
485
  const c = ctx.claudeOpts || {};
399
486
  return {
400
487
  cwd: ctx.projectDir,
@@ -425,7 +512,9 @@ function runOpts(ctx, { role, prompt, systemPrompt, allowedTools }) {
425
512
  // dispatched node/role passes through — so _phaseCtx/_nodeCtx and the
426
513
  // workspace-scan path all inherit it without per-caller edits. undefined
427
514
  // when the model carries no env (or no model is set), keeping the spawn
428
- // env byte-identical.
515
+ // env byte-identical. The sub-agent model policy deliberately does NOT
516
+ // touch this env: its only wire is the prompt block (subagentModelDirective),
517
+ // and CLAUDE_CODE_SUBAGENT_MODEL is a reserved model-env key.
429
518
  modelEnv: resolveModelEnv(c.model),
430
519
  // Guardrails: worca policy + lifted repo deny rules as {deny,...} rules ->
431
520
  // ONE --settings payload; envScrub/envAllowlist -> spawn env. All undefined
@@ -585,7 +674,7 @@ export function buildClarifyPrompt(ctx, opts = {}) {
585
674
  'pad, and never split one decision. For low-impact details, pick a sensible default rather ' +
586
675
  'than asking. If you have no material open questions, write { "questions": [] } to that ' +
587
676
  'same path.\n\n' +
588
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
677
+ fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx), subagentModel: ctxSubagentModel(ctx), endpointRouted: ctxEndpointRouted(ctx) }) +
589
678
  `Write the clarify JSON to: ${outPath}\n\n` +
590
679
  answered +
591
680
  mockMarkers({
@@ -597,161 +686,6 @@ export function buildClarifyPrompt(ctx, opts = {}) {
597
686
  );
598
687
  }
599
688
 
600
- /**
601
- * Clarify agent. Writes clarify.json; returns { questions }.
602
- * @param {import('./phases.mjs').PhaseContext} ctx
603
- * @param {{ round?: number, priorAnswers?: Array<{id,question,choice}> }} [opts]
604
- * `priorAnswers` are the Q&A already resolved in earlier rounds; injecting them
605
- * lets the planner ask only NEW questions, so the loop terminates naturally.
606
- */
607
- export async function runClarify(ctx, opts = {}) {
608
- const round = Number(opts.round) > 0 ? Number(opts.round) : 1;
609
- const priorAnswers = Array.isArray(opts.priorAnswers) ? opts.priorAnswers : [];
610
- const role = 'clarify';
611
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'clarify'), role, ctx.workspace);
612
- const prompt = buildClarifyPrompt(ctx, { round, priorAnswers });
613
-
614
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
615
-
616
- // The agent wrote clarify.json into the run dir as transient scratch; parse it
617
- // ONCE here, then make the DB the authoritative store. When ctx.pipelineId is
618
- // present (every real dispatched run) we ingest the normalized questions into the
619
- // clarify row and read them back from the row, so the planner loop consumes the DB
620
- // — not the FS file. Absent a pipelineId (pure unit ctx) we return the FS-parsed
621
- // value unchanged, so phases.mjs stays independently testable.
622
- const clarify = await readClarify(ctx.pipelineDir);
623
- if (ctx.pipelineId) {
624
- await writeClarify(ctx.pipelineId, { questions: { questions: clarify.questions } });
625
- const row = readClarifyRow(ctx.pipelineId);
626
- const questions = row.questions?.questions ?? clarify.questions;
627
- return { questions };
628
- }
629
- return { questions: clarify.questions };
630
- }
631
-
632
- /**
633
- * Planner — plan role. Writes the plan markdown; returns { planPath }.
634
- * @param {import('./phases.mjs').PhaseContext} ctx
635
- * @param {{ answers: Array<{id,question,choice}>, planFilePath: string, baseName: string }} opts
636
- */
637
- export async function runPlannerPlan(ctx, opts) {
638
- const { answers = [], planFilePath, baseName, reviewPath } = opts || {};
639
- const role = 'planner-plan';
640
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'planner'), role, ctx.workspace);
641
- const replanBlock = reviewPath
642
- ? '\n## Revise to address the review\n\n' +
643
- 'A reviewer found issues with the previous plan. Re-plan from scratch (cold start) and ' +
644
- 'address EVERY critical and major finding in the review below. Preserve the ' +
645
- '"## Clarifications (Q&A)" section.\n\n' +
646
- `Review to address: ${reviewPath}\n`
647
- : '';
648
- const prompt =
649
- taskHeader(ctx, reviewPath ? 'Revise the implementation plan' : 'Write the implementation plan') +
650
- '\n## What to do\n\n' +
651
- 'Write a complete, build-ready implementation plan. It MUST contain concrete code snippets ' +
652
- 'for the features and MUST end with a "## Clarifications (Q&A)" section reproducing the ' +
653
- 'questions and the user answers below so the reviewer can see them.\n\n' +
654
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
655
- workspaceFanOutDirective('explore', ctx.workspace, { relative: isDetachedWorkspace(ctx) }) +
656
- `Write the plan markdown to: ${planFilePath}\n` +
657
- replanBlock +
658
- '\n## Clarifications already answered\n\n' +
659
- renderAnswers(answers) +
660
- '\n' +
661
- mockMarkers({ MOCK_ROLE: role, MOCK_OUT: planFilePath, MOCK_BASE: baseName, MOCK_IN: reviewPath });
662
-
663
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
664
-
665
- return { planPath: planFilePath };
666
- }
667
-
668
- /**
669
- * Plan Refiner — one cycle. Reads inPlanPath, writes refined plan to outPlanPath and a
670
- * review JSON to reviewJsonPath. Returns { outPlanPath, review }.
671
- * @param {import('./phases.mjs').PhaseContext} ctx
672
- * @param {{ inPlanPath: string, outPlanPath: string, cycle: number, reviewJsonPath: string }} opts
673
- */
674
- export async function runRefiner(ctx, opts) {
675
- const { inPlanPath, outPlanPath, cycle, reviewJsonPath } = opts || {};
676
- const role = 'refiner';
677
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'refiner'), role, ctx.workspace);
678
- const prompt =
679
- taskHeader(ctx, `Refine the plan (cycle ${cycle})`) +
680
- '\n## What to do\n\n' +
681
- `Read the current plan, critically review it INCLUDING its code snippets, then write an ` +
682
- `improved version and a machine-readable review.\n\n` +
683
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
684
- workspaceFanOutDirective('explore', ctx.workspace, { relative: isDetachedWorkspace(ctx) }) +
685
- `Current plan to refine: ${inPlanPath}\n` +
686
- `Write the refined plan to: ${outPlanPath}\n` +
687
- `Write the review JSON to: ${reviewJsonPath}\n\n` +
688
- 'The review JSON shape is { "issues": [ { "severity", "title", "detail", "location" } ], ' +
689
- '"summary" }. Use severities critical|major|minor|suggestion. Mark a finding critical/major ' +
690
- 'only if it must be fixed before implementation.\n\n' +
691
- mockMarkers({
692
- MOCK_ROLE: role,
693
- MOCK_OUT: outPlanPath,
694
- MOCK_JSON: reviewJsonPath,
695
- MOCK_CYCLE: cycle,
696
- MOCK_IN: inPlanPath,
697
- });
698
-
699
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
700
-
701
- const review = await readReview(reviewJsonPath);
702
- return { outPlanPath, review };
703
- }
704
-
705
- /**
706
- * Decomposer — breaks the plan into vertical-slice task files + a decomposition.json
707
- * manifest. Reads planPath; writes tasks/ + decompositionPath. Returns
708
- * { decompositionPath, decomposition } where decomposition is the parsed manifest.
709
- * @param {import('./phases.mjs').PhaseContext} ctx
710
- * @param {{ planPath: string, decompositionPath: string }} opts
711
- */
712
- export async function runDecomposer(ctx, opts) {
713
- const { join, dirname } = await import('node:path');
714
- const { planPath, decompositionPath } = opts || {};
715
- const role = 'decomposer';
716
- const tasksDir = join(dirname(decompositionPath), 'tasks');
717
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'decomposer'), role, ctx.workspace);
718
- const prompt =
719
- taskHeader(ctx, 'Decompose the plan into vertical-slice tasks') +
720
- '\n## What to do\n\n' +
721
- 'Read the approved plan and break it into tracer-bullet vertical slices grouped into ' +
722
- 'ordered phases. Within a phase, tasks must be parallel-safe and edit DISJOINT files; ' +
723
- 'dependencies are expressed only as phase order. Write each task as a SELF-CONTAINED ' +
724
- 'markdown file so an implementer needs nothing but that file.\n\n' +
725
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
726
- `Plan to decompose: ${planPath}\n` +
727
- `Write each task file under: ${tasksDir}/ (name them p<phase>-t<n>-<kebab-title>.md)\n` +
728
- `Write the manifest JSON to: ${decompositionPath}\n\n` +
729
- 'The manifest shape is { "phases": [ { "ordinal", "tasks": [ { "id", "title", "file" } ] } ] }. ' +
730
- 'Use id "p<ordinal>t<n>" and a pipeline-dir-relative "file" path.\n\n' +
731
- mockMarkers({
732
- MOCK_ROLE: role,
733
- MOCK_OUT: decompositionPath,
734
- MOCK_TASKS_DIR: tasksDir,
735
- MOCK_IN: planPath,
736
- });
737
-
738
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
739
-
740
- const decomposition = await readDecomposition(decompositionPath);
741
- return { decompositionPath, decomposition };
742
- }
743
-
744
- /** Parse a decomposition.json manifest; tolerant ({phases:[]} on any error). */
745
- async function readDecomposition(path) {
746
- const { readFile } = await import('node:fs/promises');
747
- try {
748
- const raw = JSON.parse(await readFile(path, 'utf8'));
749
- return { phases: Array.isArray(raw?.phases) ? raw.phases : [] };
750
- } catch {
751
- return { phases: [] };
752
- }
753
- }
754
-
755
689
  /**
756
690
  * The shared-working-tree warning for a decomposed task that runs alongside phase
757
691
  * siblings. Parallel implementers share ONE tree with no locking, so the block
@@ -759,7 +693,7 @@ async function readDecomposition(path) {
759
693
  * git ops. Empty string when there are no siblings (solo task in its phase).
760
694
  * @param {Array<{id:string,title?:string,file?:string}>|undefined} siblings
761
695
  */
762
- function siblingsBlock(siblings) {
696
+ export function siblingsBlock(siblings) {
763
697
  if (!Array.isArray(siblings) || !siblings.length) return '';
764
698
  const lines = siblings
765
699
  .map((s) => `- ${s.id}${s.title ? ` "${s.title}"` : ''}${s.file ? ` (${s.file})` : ''}`)
@@ -815,162 +749,23 @@ export function implementerBody({ mode = 'implement', planPath, reviewPath, task
815
749
  }
816
750
 
817
751
  /**
818
- * Implementerimplement or fix. Returns { summary }.
819
- * @param {import('./phases.mjs').PhaseContext} ctx
820
- * @param {{ planPath: string, reviewPath?: string, taskPath?: string, siblings?: Array<{id:string,title?:string,file?:string}>, mode: "implement"|"fix" }} opts
821
- */
822
- export async function runImplementer(ctx, opts) {
823
- const { planPath, reviewPath, taskPath, siblings, mode = 'implement' } = opts || {};
824
- const role = 'implementer';
825
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'implementer'), role, ctx.workspace);
826
-
827
- const body = implementerBody({ mode, planPath, reviewPath, taskPath, siblings });
828
-
829
- const prompt =
830
- taskHeader(ctx, mode === 'fix' ? 'Fix the implementation' : 'Implement the plan') +
831
- '\n## What to do\n\n' +
832
- body +
833
- '\n' +
834
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
835
- workspaceFanOutDirective('task', ctx.workspace, { relative: isDetachedWorkspace(ctx) }) +
836
- 'Work inside the project directory (your cwd). Commit nothing; just edit files and tests.\n\n' +
837
- mockMarkers({ MOCK_ROLE: role, MOCK_IN: taskPath || planPath, MOCK_OUT: reviewPath });
838
-
839
- const { text } = await runClaude(
840
- runOpts(ctx, { role, prompt, systemPrompt, allowedTools: IMPLEMENTER_TOOLS }),
841
- );
842
-
843
- const summary = (text || '').trim() || `Implementer (${mode}) completed.`;
844
- return { summary };
845
- }
846
-
847
- /**
848
- * Code Reviewer — one cycle. Writes review markdown + review JSON. Returns { review }.
849
- * @param {import('./phases.mjs').PhaseContext} ctx
850
- * @param {{ planPath: string, reviewMdPath: string, reviewJsonPath: string, cycle: number }} opts
752
+ * The reviewer's diff instruction extracted VERBATIM from runReviewer so the v2
753
+ * executor's `as: 'worktree'` renderer resolves to the same bytes. Prefer diffing
754
+ * against the recorded checkpoint commit: new files are made visible via the
755
+ * orchestrator's intent-to-add staging after each implement pass, so
756
+ * `git diff <ref>` and `git status` both show greenfield work. Pure + exported.
757
+ * @param {{checkpointRef?:string}} [ctx]
758
+ * @returns {string}
851
759
  */
852
- export async function runReviewer(ctx, opts) {
853
- const { planPath, reviewMdPath, reviewJsonPath, cycle } = opts || {};
854
- const role = 'reviewer';
855
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'reviewer'), role, ctx.workspace);
856
- // Prefer diffing against the recorded checkpoint commit. New files are made
857
- // visible via the orchestrator's intent-to-add staging after each implement
858
- // pass, so `git diff <ref>` and `git status` both show greenfield work.
859
- const ref = (ctx.checkpointRef || '').trim();
860
- const diffInstruction = ref
760
+ export function diffInstruction(ctx) {
761
+ const ref = String(ctx?.checkpointRef || '').trim();
762
+ return ref
861
763
  ? `Inspect the diff with \`git diff ${ref}\` (the orchestrator's pre-implementation ` +
862
764
  `checkpoint) and \`git status\` in your cwd. New/untracked files are intent-to-added, ` +
863
765
  `so they DO appear in that diff; use \`git status\` to cross-check.`
864
766
  : 'Inspect the diff with `git diff` and `git status` in your cwd. If `git diff` looks ' +
865
767
  'empty, the changes may be newly-created files — confirm with `git status` and ' +
866
768
  '`git diff HEAD`.';
867
- const prompt =
868
- taskHeader(ctx, `Review the implementation (cycle ${cycle})`) +
869
- '\n## What to do\n\n' +
870
- 'Review the git diff of what was implemented against the plan. Write a human-readable review ' +
871
- 'markdown AND a machine-readable review JSON. ' +
872
- diffInstruction +
873
- '\n\n' +
874
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
875
- workspaceFanOutDirective('review', ctx.workspace, { relative: isDetachedWorkspace(ctx) }) +
876
- `Plan that was implemented: ${planPath}\n` +
877
- `Write the review markdown to: ${reviewMdPath}\n` +
878
- `Write the review JSON to: ${reviewJsonPath}\n\n` +
879
- 'The review JSON shape is { "issues": [ { "severity", "title", "detail", "location" } ], ' +
880
- '"summary" }. Use severities critical|major|minor|suggestion; only critical/major block the ' +
881
- 'pipeline.\n\n' +
882
- mockMarkers({
883
- MOCK_ROLE: role,
884
- MOCK_OUT: reviewMdPath,
885
- MOCK_JSON: reviewJsonPath,
886
- MOCK_CYCLE: cycle,
887
- });
888
-
889
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
890
-
891
- const review = await readReview(reviewJsonPath);
892
- return { review };
893
- }
894
-
895
- /**
896
- * Plan Reviewer — one cycle. Reviews the PLAN markdown (no git diff). Writes a review
897
- * markdown + review JSON. Returns { review }.
898
- * @param {import('./phases.mjs').PhaseContext} ctx
899
- * @param {{ planPath: string, reviewMdPath: string, reviewJsonPath: string, cycle: number }} opts
900
- */
901
- export async function runPlanReviewer(ctx, opts) {
902
- const { planPath, reviewMdPath, reviewJsonPath, cycle } = opts || {};
903
- const role = 'plan-review';
904
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'planReviewer'), role, ctx.workspace);
905
- const prompt =
906
- taskHeader(ctx, `Review the plan (cycle ${cycle})`) +
907
- '\n## What to do\n\n' +
908
- 'Review the implementation PLAN against the original request and the real codebase. Do NOT ' +
909
- 'rewrite the plan. Write a human-readable review markdown AND a machine-readable review JSON.\n\n' +
910
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
911
- workspaceFanOutDirective('explore', ctx.workspace, { relative: isDetachedWorkspace(ctx) }) +
912
- `Plan to review: ${planPath}\n` +
913
- `Write the review markdown to: ${reviewMdPath}\n` +
914
- `Write the review JSON to: ${reviewJsonPath}\n\n` +
915
- 'The review JSON shape is { "issues": [ { "severity", "title", "detail", "location" } ], ' +
916
- '"summary" }. Use severities critical|major|minor|suggestion; only critical/major block (the ' +
917
- 'planner then revises).\n\n' +
918
- mockMarkers({
919
- MOCK_ROLE: role,
920
- MOCK_OUT: reviewMdPath,
921
- MOCK_JSON: reviewJsonPath,
922
- MOCK_CYCLE: cycle,
923
- MOCK_IN: planPath,
924
- });
925
-
926
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
927
-
928
- const review = await readReview(reviewJsonPath);
929
- return { review };
930
- }
931
-
932
- /**
933
- * Workspace Reviewer — verifier (in-pipeline, loopSource). The workspace-run
934
- * replacement for runReviewer: fan out one reviewer sub-agent per CHANGED member
935
- * (each diffing `checkpointRefs[projectKey]...feature` inside that member's
936
- * worktree — the `## Workspace projects` block in the task header names each
937
- * worktree dir + checkpoint), then synthesize ONE review markdown + ONE
938
- * review-cycleN.json that is the UNION of every critical/major issue, sorted by
939
- * projectKey then severity. Reuses protocol.readReview / hasBlocking unchanged, so
940
- * the orchestrator's review->implementer loop gates identically. Returns { review }.
941
- * @param {import('./phases.mjs').PhaseContext} ctx
942
- * @param {{ planPath: string, reviewMdPath: string, reviewJsonPath: string, cycle: number }} opts
943
- */
944
- export async function runWorkspaceReviewer(ctx, opts) {
945
- const { planPath, reviewMdPath, reviewJsonPath, cycle } = opts || {};
946
- const role = 'workspace-reviewer';
947
- // The body is the contract (C10: no FALLBACK_PROMPTS entry); the system prompt
948
- // ALSO carries the `## Workspace Context` block via ctx.workspace.
949
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, resolveAgentBody(ctx, 'workspaceReviewer'), role, ctx.workspace);
950
- const prompt =
951
- taskHeader(ctx, `Review the workspace implementation (cycle ${cycle})`) +
952
- '\n## What to do\n\n' +
953
- 'Review what was implemented across the member projects against the plan. Write a SINGLE ' +
954
- 'human-readable review markdown AND a SINGLE machine-readable review JSON.\n\n' +
955
- workspaceFanOutDirective('review', ctx.workspace, { relative: isDetachedWorkspace(ctx) }) +
956
- `Plan that was implemented: ${planPath}\n` +
957
- `Write the merged review markdown to: ${reviewMdPath}\n` +
958
- `Write the merged review JSON to: ${reviewJsonPath}\n\n` +
959
- 'The review JSON shape is { "issues": [ { "severity", "title", "detail", "location" } ], ' +
960
- '"summary" }. Use severities critical|major|minor|suggestion; only critical/major block the ' +
961
- 'pipeline. The issue list is the UNION of every per-project critical/major issue (never ' +
962
- 'collapse one), sorted by projectKey then severity, each location prefixed "<projectKey>: ".\n\n' +
963
- mockMarkers({
964
- MOCK_ROLE: role,
965
- MOCK_OUT: reviewMdPath,
966
- MOCK_JSON: reviewJsonPath,
967
- MOCK_CYCLE: cycle,
968
- });
969
-
970
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
971
-
972
- const review = await readReview(reviewJsonPath);
973
- return { review };
974
769
  }
975
770
 
976
771
  /**
@@ -1014,7 +809,7 @@ export async function runWorkspaceScan(ctx, opts = {}) {
1014
809
  // the scanner is OFF-pipeline (it runs before any run root exists, with cwd inside a
1015
810
  // member's real dir), so its project `.claude/agents` really are discoverable (§8.21
1016
811
  // covers run-root cwds only).
1017
- fanOutDirective(true) +
812
+ fanOutDirective(true, { endpointRouted: ctxEndpointRouted(ctx) }) +
1018
813
  'Dispatch ONE read-only investigator per member project (cap 8); merge their reports in sorted ' +
1019
814
  '`projectKey` order and synthesize the single description yourself. Investigators MUST NOT ' +
1020
815
  're-fan-out.\n\n' +
@@ -1053,91 +848,6 @@ export async function runWorkspaceScan(ctx, opts = {}) {
1053
848
  return { description, outPath };
1054
849
  }
1055
850
 
1056
- /**
1057
- * Manual Tests Checklist — producer. Reads the plan (and any implementation diff)
1058
- * and writes a markdown checklist of manual test cases as a pipeline artifact.
1059
- * Returns { checklistPath, summary }.
1060
- * @param {import('./phases.mjs').PhaseContext} ctx
1061
- * @param {{ planPath: string, checklistPath: string }} opts
1062
- */
1063
- export async function runManualTestsChecklist(ctx, opts) {
1064
- const { planPath, checklistPath } = opts || {};
1065
- const role = 'manual-tests-checklist';
1066
- const systemPrompt = buildSystemPrompt(
1067
- ctx.toolInstruction,
1068
- resolveAgentBody(ctx, 'manualTestsChecklist'),
1069
- role,
1070
- ctx.workspace,
1071
- );
1072
- // §5.8: at a run-root cwd there is no working tree to `git diff`, so the changes
1073
- // are named PER MEMBER. Single mode (both modes) and legacy workspace runs keep
1074
- // today's byte-identical sentence.
1075
- const perMemberDiff = workspaceDiffInstruction(ctx);
1076
- const changesInstruction = perMemberDiff
1077
- ? 'Read the implementation plan and the implemented changes in EVERY member checkout — your ' +
1078
- 'cwd is the worca-cc run root, not a repository, so inspect each member on its own:\n\n' +
1079
- perMemberDiff +
1080
- '\n\nThen write a markdown checklist of concrete manual test cases a human can run against ' +
1081
- 'the app. Each case: a `- [ ]` line with steps and the expected result.\n\n'
1082
- : 'Read the implementation plan and the implemented changes (via `git diff` in your cwd), ' +
1083
- 'then write a markdown checklist of concrete manual test cases a human can run against the ' +
1084
- 'app. Each case: a `- [ ]` line with steps and the expected result.\n\n';
1085
- const prompt =
1086
- taskHeader(ctx, 'Draft a manual test checklist') +
1087
- '\n## What to do\n\n' +
1088
- changesInstruction +
1089
- `Plan: ${planPath}\n` +
1090
- `Write the checklist markdown to: ${checklistPath}\n\n` +
1091
- mockMarkers({ MOCK_ROLE: role, MOCK_OUT: checklistPath, MOCK_IN: planPath });
1092
-
1093
- const { text } = await runClaude(
1094
- runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }),
1095
- );
1096
-
1097
- const summary = (text || '').trim() || 'Manual test checklist written.';
1098
- return { checklistPath, summary };
1099
- }
1100
-
1101
- /**
1102
- * Manual web UI testing — verifier (loopSource). Drives the running web UI through
1103
- * the manual checklist (Playwright MCP, declared in the agent frontmatter) and
1104
- * emits the protocol review verdict JSON. Returns { review }.
1105
- * @param {import('./phases.mjs').PhaseContext} ctx
1106
- * @param {{ checklistPath: string, reviewMdPath: string, reviewJsonPath: string, cycle: number }} opts
1107
- */
1108
- export async function runManualWebUiTesting(ctx, opts) {
1109
- const { checklistPath, reviewMdPath, reviewJsonPath, cycle } = opts || {};
1110
- const role = 'manual-web-ui-testing';
1111
- const systemPrompt = buildSystemPrompt(
1112
- ctx.toolInstruction,
1113
- resolveAgentBody(ctx, 'manualWebUiTesting'),
1114
- role,
1115
- ctx.workspace,
1116
- );
1117
- const prompt =
1118
- taskHeader(ctx, `Run the manual web UI tests (cycle ${cycle})`) +
1119
- '\n## What to do\n\n' +
1120
- 'Execute the manual test checklist against the running web UI using the Playwright tools. ' +
1121
- 'Write a human-readable result markdown AND a machine-readable review JSON.\n\n' +
1122
- `Checklist to run: ${checklistPath}\n` +
1123
- `Write the result markdown to: ${reviewMdPath}\n` +
1124
- `Write the review JSON to: ${reviewJsonPath}\n\n` +
1125
- 'The review JSON shape is { "issues": [ { "severity", "title", "detail", "location" } ], ' +
1126
- '"summary" }. Use severities critical|major|minor|suggestion; only critical/major block the ' +
1127
- 'pipeline (a failing manual case is at least major).\n\n' +
1128
- mockMarkers({
1129
- MOCK_ROLE: role,
1130
- MOCK_OUT: reviewMdPath,
1131
- MOCK_JSON: reviewJsonPath,
1132
- MOCK_CYCLE: cycle,
1133
- });
1134
-
1135
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
1136
-
1137
- const review = await readReview(reviewJsonPath);
1138
- return { review };
1139
- }
1140
-
1141
851
  // ── generic runners (metadata-declared agents, zero bespoke core code) ──────────
1142
852
 
1143
853
  /**
@@ -1184,86 +894,15 @@ export function genericIoBlock(inputs = {}, outputs = {}) {
1184
894
  );
1185
895
  }
1186
896
 
1187
- /**
1188
- * Generic producer — any metadata-declared producer with no bespoke branch.
1189
- * Prompt = taskHeader + role hints + Inputs/Outputs channel->path lists; the
1190
- * system prompt body is the agent's own .md (node.agentPrompt). Returns { summary }.
1191
- */
1192
- export async function runGenericProducer(ctx) {
1193
- const key = ctx.node?.key || 'agent';
1194
- const role = `generic:${key}`; // no FALLBACK entry: the .md body is the contract
1195
- const body = resolveAgentBody(ctx, key);
1196
- if (!String(body || '').trim()) {
1197
- console.warn(`[phases] generic producer "${key}": no agent .md body resolved — running with an empty system prompt`);
1198
- }
1199
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, body, role, ctx.workspace);
1200
- const outputs = ctx.outputs || {};
1201
- const primary = Object.values(outputs).find((h) => h && h.path)?.path;
1202
- const hints = (ctx.node?.promptHints || '').trim();
1203
- const prompt =
1204
- taskHeader(ctx, `Run agent "${key}"`) +
1205
- '\n## What to do\n\n' +
1206
- 'You are a pipeline agent. Read every input below, do your job exactly as your role ' +
1207
- 'instructions describe, and write EVERY declared output to its exact path.\n\n' +
1208
- (hints ? hints + '\n\n' : '') +
1209
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
1210
- genericIoBlock(ctx.inputs, outputs) +
1211
- mockMarkers({ MOCK_ROLE: 'generic-producer', MOCK_OUT: primary, MOCK_CYCLE: ctx.cycle });
1212
-
1213
- const { text } = await runClaude(
1214
- runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }),
1215
- );
1216
- return { summary: (text || '').trim() || `Agent ${key} completed.` };
1217
- }
1218
-
1219
- /**
1220
- * Generic verifier — any metadata-declared verifier with no bespoke branch. Emits
1221
- * the standard protocol review (md + json); paths come from the allocated `review`
1222
- * output (pipeline-local `<key>-review-cycleN.*` when the node mints no review).
1223
- * Returns { review, reviewMdPath } for runners.verifier's verdict wrap.
1224
- */
1225
- export async function runGenericVerifier(ctx) {
1226
- const key = ctx.node?.key || 'agent';
1227
- const role = `generic:${key}`;
1228
- const body = resolveAgentBody(ctx, key);
1229
- if (!String(body || '').trim()) {
1230
- console.warn(`[phases] generic verifier "${key}": no agent .md body resolved — running with an empty system prompt`);
1231
- }
1232
- const systemPrompt = buildSystemPrompt(ctx.toolInstruction, body, role, ctx.workspace);
1233
- const cycle = Number(ctx.cycle) > 0 ? Number(ctx.cycle) : 1;
1234
- const { review: reviewOut, ...otherOutputs } = ctx.outputs || {};
1235
- const reviewMdPath = reviewOut?.mdPath ?? joinPipeline(ctx.pipelineDir, `${key}-review-cycle${cycle}.md`);
1236
- const reviewJsonPath = reviewOut?.jsonPath ?? joinPipeline(ctx.pipelineDir, `${key}-review-cycle${cycle}.json`);
1237
- const hints = (ctx.node?.promptHints || '').trim();
1238
- // Route the (possibly fallback-pathed) review handle through the IO block so the
1239
- // Outputs section never renders the "(none — report as final message)" placeholder
1240
- // in contradiction with the review-write instructions that follow.
1241
- const ioOutputs = { ...otherOutputs, review: { kind: 'review', mdPath: reviewMdPath, jsonPath: reviewJsonPath } };
1242
- const prompt =
1243
- taskHeader(ctx, `Verify: ${key} (cycle ${cycle})`) +
1244
- '\n## What to do\n\n' +
1245
- 'You are a verifier. Inspect the inputs below exactly as your role instructions describe, ' +
1246
- 'then write a human-readable review markdown AND a machine-readable review JSON.\n\n' +
1247
- (hints ? hints + '\n\n' : '') +
1248
- fanOutDirective(ctxFanOut(ctx), { omitProjectAgents: isDetachedWorkspace(ctx) }) +
1249
- genericIoBlock(ctx.inputs, ioOutputs) +
1250
- 'The review JSON shape is { "issues": [ { "severity", "title", "detail", "location" } ], ' +
1251
- '"summary" }. Use severities critical|major|minor|suggestion; only critical/major block the ' +
1252
- 'pipeline.\n\n' +
1253
- mockMarkers({ MOCK_ROLE: 'generic-verifier', MOCK_OUT: reviewMdPath, MOCK_JSON: reviewJsonPath, MOCK_CYCLE: cycle });
1254
-
1255
- await runClaude(runOpts(ctx, { role, prompt, systemPrompt, allowedTools: READ_WRITE_TOOLS }));
1256
-
1257
- const review = await readReview(reviewJsonPath);
1258
- return { review, reviewMdPath };
1259
- }
1260
-
1261
897
  // ── small local helpers ────────────────────────────────────────────────────────
1262
898
 
1263
899
  /** Join a file name onto the pipeline dir without importing node:path's full surface. */
1264
900
  function joinPipeline(pipelineDir, name) {
1265
- const base = String(pipelineDir || '').replace(/\/+$/, '');
1266
- return `${base}/${name}`;
901
+ // Native separator: this builds a real filesystem path (writeFile targets and
902
+ // paths compared with join()-built values elsewhere). A hardcoded '/' produced
903
+ // a mixed-separator path on Windows; on POSIX join() is byte-identical to the
904
+ // old '/' form, so no non-Windows behaviour changes.
905
+ return join(String(pipelineDir || '').replace(/[\\/]+$/, ''), name);
1267
906
  }
1268
907
 
1269
908
  /** Render the answered clarifications as a markdown Q&A list for the plan prompt. */
@@ -1277,3 +916,35 @@ export function renderAnswers(answers) {
1277
916
  .join('\n') + '\n'
1278
917
  );
1279
918
  }
919
+
920
+ /**
921
+ * Render the `## Attached files` block listing each attachment by path + name.
922
+ * Single source of truth shared by renderPromptArtifact (the seeded file body) and
923
+ * phases.mjs taskHeader (the entry agent's inline header) so the two cannot drift.
924
+ * Returns '' when there are no attachments.
925
+ * @param {Array<{name:string,path:string}>} [extras]
926
+ */
927
+ export function renderAttachmentsBlock(extras = []) {
928
+ if (!Array.isArray(extras) || extras.length === 0) return '';
929
+ return (
930
+ `\n## Attached files\n\nThe user attached these files; read any that are relevant:\n\n` +
931
+ extras.map((e) => `- \`${e.path}\` (${e.name})`).join('\n') +
932
+ '\n'
933
+ );
934
+ }
935
+
936
+ /**
937
+ * Render the markdown a seeded artifact channel holds: the user's request stands in
938
+ * for the missing upstream artifact, with any attached files listed by path.
939
+ * @param {string} promptText
940
+ * @param {Array<{name:string,path:string}>} [extras]
941
+ */
942
+ export function renderPromptArtifact(promptText, extras = []) {
943
+ const body = (promptText || '').trim() || '(no prompt text)';
944
+ return (
945
+ `# Task (from the user prompt)\n\n` +
946
+ `No upstream agent produced this artifact, so the user's request below stands in for it.\n\n` +
947
+ `## Original request\n\n${body}\n` +
948
+ renderAttachmentsBlock(extras)
949
+ );
950
+ }