@popoverai/dotrequirements 0.24.2 → 0.25.0

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 (35) hide show
  1. package/README.md +7 -9
  2. package/dist/codebase-to-spec/cache.d.ts +6 -0
  3. package/dist/codebase-to-spec/cache.js +1 -0
  4. package/dist/codebase-to-spec/dispatch.d.ts +115 -0
  5. package/dist/codebase-to-spec/dispatch.js +850 -0
  6. package/dist/codebase-to-spec/pack.d.ts +7 -0
  7. package/dist/codebase-to-spec/pack.js +29 -8
  8. package/dist/codebase-to-spec/prompts/editor.d.ts +1 -1
  9. package/dist/codebase-to-spec/prompts/editor.js +1 -1
  10. package/dist/codebase-to-spec/prompts/specifier.d.ts +1 -1
  11. package/dist/codebase-to-spec/prompts/specifier.js +3 -2
  12. package/dist/codebase-to-spec/schemas.d.ts +528 -0
  13. package/dist/codebase-to-spec/schemas.js +244 -0
  14. package/dist/codebase-to-spec/skill-install.d.ts +41 -14
  15. package/dist/codebase-to-spec/skill-install.js +75 -26
  16. package/dist/commands/codebase-to-spec/compose-orchestrator.d.ts +14 -0
  17. package/dist/commands/codebase-to-spec/compose-orchestrator.js +54 -0
  18. package/dist/commands/codebase-to-spec/dispatch-context.d.ts +9 -0
  19. package/dist/commands/codebase-to-spec/dispatch-context.js +19 -0
  20. package/dist/commands/codebase-to-spec/dispatch-editor.d.ts +15 -0
  21. package/dist/commands/codebase-to-spec/dispatch-editor.js +70 -0
  22. package/dist/commands/codebase-to-spec/dispatch-planner.d.ts +18 -0
  23. package/dist/commands/codebase-to-spec/dispatch-planner.js +89 -0
  24. package/dist/commands/codebase-to-spec/dispatch-spec.d.ts +13 -0
  25. package/dist/commands/codebase-to-spec/dispatch-spec.js +56 -0
  26. package/dist/commands/codebase-to-spec/index.js +58 -2
  27. package/dist/commands/codebase-to-spec/pack.d.ts +5 -0
  28. package/dist/commands/codebase-to-spec/pack.js +6 -3
  29. package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +20 -0
  30. package/dist/commands/codebase-to-spec/present-orchestrator.js +81 -0
  31. package/dist/commands/codebase-to-spec/skill-install.js +5 -1
  32. package/dist/templates/agents/cts-worker.md +9 -0
  33. package/dist/templates/skills/codebase-to-spec/SKILL.md +44 -77
  34. package/dist/templates/workflows/specify-codebase.js +372 -0
  35. package/package.json +2 -2
@@ -0,0 +1,850 @@
1
+ /**
2
+ * Dispatch-context composition for the codebase-to-spec workflow.
3
+ *
4
+ * A cts-worker runs `dotrequirements cts dispatch-context <dispatch-id>` to
5
+ * fetch the full composed prompt for its role (self-composition — there is no
6
+ * PreToolUse hook). Each returned prompt is self-contained: persona body +
7
+ * paths + any prior-round context the worker needs.
8
+ *
9
+ * Recognized dispatch IDs:
10
+ * - `planner-initial` — initial planner; writes outline.yaml
11
+ * - `planner-revise-<turn>` — revising planner; reads the prior outline.yaml
12
+ * (with its review.thread), applies the latest thread entry's revisions, and
13
+ * writes a new outline.yaml. `<turn>` is the round being PRODUCED (2 = first
14
+ * revision).
15
+ * - `outline-reviewer` — independent reviewer of the area decomposition; appends
16
+ * its verdict to the document-level review.thread.
17
+ * - `specifier-<area>` — drafts one area's partial.
18
+ * - `reviewer-<area>` — independent per-area reviewer; appends its verdict to
19
+ * that area's review.thread.
20
+ * - `editor-<area>` — applies the per-area reviewer's revisions to the partial.
21
+ * - `cross-area-reviewer` — document-level reviewer of the composed spec
22
+ * (cross-area issues + coverage); returns its verdict for the reconcile loop.
23
+ * - `compose-editor` — applies cross-area revisions to the composed spec.
24
+ * - `phase1-test` — mechanical plumbing self-test.
25
+ *
26
+ * Requirements covered:
27
+ * - CTSO-CONV-2: the outline converges via an independent reviewer loop
28
+ * - CTSO-CONV-5: cross-area review reconciles the composed spec
29
+ */
30
+ import { existsSync, readFileSync } from "node:fs";
31
+ import { findProjectRoot } from "../utils/project-settings.js";
32
+ import { cachePaths } from "./cache.js";
33
+ import { sanitizeAreaName } from "./fan-out.js";
34
+ import { EDITOR_PROMPT } from "./prompts/editor.js";
35
+ import { PLANNER_INITIAL_PROMPT } from "./prompts/planner-initial.js";
36
+ import { PLANNER_REVISE_PROMPT } from "./prompts/planner-revise.js";
37
+ import { SPEC_REVIEWER_PROMPT } from "./prompts/spec-reviewer.js";
38
+ import { SPECIFIER_PROMPT } from "./prompts/specifier.js";
39
+ import { parseConversationalOutline, } from "./schemas.js";
40
+ /** Phase 1 dispatch-id used to validate the mechanical plumbing end-to-end. */
41
+ export const PHASE1_TEST_DISPATCH_ID = "phase1-test";
42
+ /** Phase 1 token the test worker echoes back. */
43
+ export const PHASE1_TEST_TOKEN = "PHASE1-WORKER-OK token=CTS9X";
44
+ /** Phase 2 dispatch-id for the initial planner pass. */
45
+ export const PLANNER_INITIAL_DISPATCH_ID = "planner-initial";
46
+ /**
47
+ * Phase 2b dispatch-id prefix for planner revise dispatches. Suffix is the
48
+ * turn number being PRODUCED (e.g., `planner-revise-2` produces the
49
+ * second turn's outline, reading the prior outline + its latest review
50
+ * thread entry as inputs).
51
+ */
52
+ export const PLANNER_REVISE_DISPATCH_ID_PREFIX = "planner-revise-";
53
+ /**
54
+ * Phase 3 dispatch-id prefix for specifier dispatches. Suffix is the area's
55
+ * prefix from the approved outline (e.g., `specifier-PLAN` runs the
56
+ * specifier for the PLAN area).
57
+ */
58
+ export const SPECIFIER_DISPATCH_ID_PREFIX = "specifier-";
59
+ /**
60
+ * Phase 4 dispatch-id prefix for editor dispatches. Suffix is the area's
61
+ * prefix (e.g., `editor-PLAN` revises the PLAN area's partial based on
62
+ * the latest area.review.thread entry's revisions).
63
+ */
64
+ export const EDITOR_DISPATCH_ID_PREFIX = "editor-";
65
+ /**
66
+ * Dispatch-id prefix for per-area reviewer dispatches. Suffix is the area's
67
+ * prefix (e.g., `reviewer-PLAN` reviews the PLAN area's partial). The reviewer
68
+ * is a distinct worker from the specifier/editor that produced the partial
69
+ * (independent second pair of eyes — CTSO-CONV-3.1). It appends its verdict to
70
+ * that area's `review.thread` in outline.yaml (read by the editor dispatch) and
71
+ * also returns the verdict so the workflow's convergence loop can branch on it.
72
+ */
73
+ export const REVIEWER_DISPATCH_ID_PREFIX = "reviewer-";
74
+ /**
75
+ * Dispatch-id for the outline reviewer — the independent reviewer for the
76
+ * planner's area decomposition (project level, one per run). Distinct from the
77
+ * planner that produced the outline (CTSO-CONV-2.1). It appends its verdict to
78
+ * the project-level `outline.review.thread` and returns the verdict so the
79
+ * workflow's outline loop can branch on it. The outline loop is sequential, so
80
+ * unlike the per-area reviewers this writer never contends on the file.
81
+ */
82
+ export const OUTLINE_REVIEWER_DISPATCH_ID = "outline-reviewer";
83
+ /**
84
+ * Dispatch-id for the cross-area reviewer — the document-level reviewer that
85
+ * runs AFTER per-area work converges and the partials are composed into a single
86
+ * spec. It reviews the composed spec for cross-area issues (duplication,
87
+ * terminology/persona drift, awkward cross-cutting splits, depth imbalance) plus
88
+ * document-level coverage and framing. It appends its verdict to the outline's
89
+ * top-level `crossAreaReview.thread` (auditable, like the per-area reviewers)
90
+ * and also returns it so the workflow's reconcile loop can branch on it
91
+ * (CTSO-CONV-5). Reuses the legacy SPEC_REVIEWER_PROMPT persona. One per round,
92
+ * sequential — no file contention.
93
+ */
94
+ export const CROSS_AREA_REVIEWER_DISPATCH_ID = "cross-area-reviewer";
95
+ /**
96
+ * Dispatch-id for the compose-level editor — applies the cross-area reviewer's
97
+ * revisions to the whole composed spec (not one area). Reuses the legacy
98
+ * EDITOR_PROMPT persona at its native document scope, reading the revisions to
99
+ * apply from the outline's top-level `crossAreaReview.thread` — the same
100
+ * auditable channel the per-area editor uses for `area.review.thread`.
101
+ */
102
+ export const COMPOSE_EDITOR_DISPATCH_ID = "compose-editor";
103
+ /**
104
+ * Compose the dispatch context for a given dispatch-id.
105
+ *
106
+ * @returns the composed dispatch context, or `null` if the id is unknown
107
+ * or required inputs (cache files) are missing.
108
+ */
109
+ export function composeDispatchContext(dispatchId, options = {}) {
110
+ if (dispatchId === PHASE1_TEST_DISPATCH_ID) {
111
+ return composePhase1Test();
112
+ }
113
+ if (dispatchId === PLANNER_INITIAL_DISPATCH_ID) {
114
+ return composePlannerInitial(options);
115
+ }
116
+ if (dispatchId.startsWith(PLANNER_REVISE_DISPATCH_ID_PREFIX)) {
117
+ const turnText = dispatchId.slice(PLANNER_REVISE_DISPATCH_ID_PREFIX.length);
118
+ const turn = Number.parseInt(turnText, 10);
119
+ if (!Number.isFinite(turn) || turn < 2 || String(turn) !== turnText) {
120
+ return null;
121
+ }
122
+ return composePlannerRevise(turn, options);
123
+ }
124
+ if (dispatchId.startsWith(SPECIFIER_DISPATCH_ID_PREFIX)) {
125
+ const areaPrefix = dispatchId.slice(SPECIFIER_DISPATCH_ID_PREFIX.length);
126
+ if (!areaPrefix)
127
+ return null;
128
+ return composeSpecifier(areaPrefix, options);
129
+ }
130
+ if (dispatchId === OUTLINE_REVIEWER_DISPATCH_ID) {
131
+ return composeOutlineReviewer(options);
132
+ }
133
+ if (dispatchId === CROSS_AREA_REVIEWER_DISPATCH_ID) {
134
+ return composeCrossAreaReviewer(options);
135
+ }
136
+ if (dispatchId === COMPOSE_EDITOR_DISPATCH_ID) {
137
+ return composeComposeEditor(options);
138
+ }
139
+ if (dispatchId.startsWith(REVIEWER_DISPATCH_ID_PREFIX)) {
140
+ const areaPrefix = dispatchId.slice(REVIEWER_DISPATCH_ID_PREFIX.length);
141
+ if (!areaPrefix)
142
+ return null;
143
+ return composeReviewer(areaPrefix, options);
144
+ }
145
+ // NOTE: the editor branch must come after the reviewer branch only if their
146
+ // prefixes could collide; they don't ("editor-" vs "reviewer-"), so order is
147
+ // irrelevant here.
148
+ if (dispatchId.startsWith(EDITOR_DISPATCH_ID_PREFIX)) {
149
+ const areaPrefix = dispatchId.slice(EDITOR_DISPATCH_ID_PREFIX.length);
150
+ if (!areaPrefix)
151
+ return null;
152
+ return composeEditor(areaPrefix, options);
153
+ }
154
+ return null;
155
+ }
156
+ function composePhase1Test() {
157
+ return {
158
+ prompt: [
159
+ "You are the Phase 1 mechanical-plumbing test worker for the",
160
+ "codebase-to-spec conversational orchestrator.",
161
+ "",
162
+ "Reply with exactly the following text and nothing else:",
163
+ "",
164
+ PHASE1_TEST_TOKEN,
165
+ ].join("\n"),
166
+ };
167
+ }
168
+ /**
169
+ * YAML overlay applied on top of both planner prompts. Overrides their
170
+ * "output JSON" instructions and adds per-area customer guidance.
171
+ *
172
+ * Designed as a single shared block since the schema, customer guidance,
173
+ * and format override apply identically to initial + revise dispatches.
174
+ */
175
+ const PLANNER_YAML_OVERLAY = `---
176
+
177
+ ## Output format — read carefully, this OVERRIDES the system prompt above
178
+
179
+ The system prompt above instructs you to output a JSON object. **This dispatch overrides that.** Your output format is YAML, not JSON. Follow the YAML schema below.
180
+
181
+ The schema also includes per-area \`customers\` and renames \`files\` to \`source_files\`. Both are new fields not present in the system prompt's schema description.
182
+
183
+ ## YAML output schema
184
+
185
+ Your output is a YAML document with this structure (every field below is required unless marked optional):
186
+
187
+ \`\`\`yaml
188
+ title: <string>
189
+ defaultPrefix: <uppercase identifier, distinct from any area prefix>
190
+ summary: |
191
+ <multi-line prose summarizing the system: what it is, who it's for>
192
+ review: # OPTIONAL — present only when revising; preserve from input verbatim
193
+ result: approved | needs-revision
194
+ thread:
195
+ - result: approved | needs-revision
196
+ revisions: # only when result is needs-revision; ≥1 entries
197
+ - <string>
198
+ areas:
199
+ - name: <string — read in customer vocabulary>
200
+ prefix: <uppercase identifier, unique within document>
201
+ description: |
202
+ <multi-line prose grounding this area in customer behavior>
203
+ source_files: [<file paths from the pack>]
204
+ customers:
205
+ - description: |
206
+ <multi-line prose: SPECIFIC, area-scoped customer description>
207
+ \`\`\`
208
+
209
+ ## Per-area customer guidance (NEW field — read carefully)
210
+
211
+ For EACH area, identify the customer(s) who care about that area's behaviors. Write each customer description fresh, specific to who interacts with THAT area:
212
+
213
+ - Not "developer" but "a CTS operator at the planning checkpoint, deciding whether CTS understood their codebase well enough to fan-out specifiers"
214
+ - Not "user" but the concrete role and the specific situation they're in when this area's behaviors matter
215
+ - The same role appearing in multiple areas may have DIFFERENT needs at different moments; describe each occurrence freshly
216
+ - If you find yourself writing the same generic description across areas, that's a smell — either your customers aren't specific enough OR your areas aren't customer-distinct
217
+
218
+ There is no project-level customer list. Each area's customers are defined inline, area-scoped.
219
+
220
+ ## Schema validation reminders
221
+
222
+ - \`defaultPrefix\` and each area's \`prefix\` must be uppercase alphanumeric/underscore (regex: \`/^[A-Z][A-Z0-9_]*$/\`).
223
+ - Area prefixes must be unique within the document AND distinct from \`defaultPrefix\`.
224
+ - \`source_files\` paths must match the \`File: <path>\` headers in the pack exactly.
225
+ - Each area must have ≥1 customer with a non-empty \`description\`.
226
+ `;
227
+ function composePlannerInitial(options) {
228
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
229
+ const paths = cachePaths(projectRoot);
230
+ if (!existsSync(paths.overview)) {
231
+ return null;
232
+ }
233
+ const prompt = [
234
+ PLANNER_INITIAL_PROMPT,
235
+ "",
236
+ PLANNER_YAML_OVERLAY,
237
+ "",
238
+ "---",
239
+ "",
240
+ "## Your task for this dispatch",
241
+ "",
242
+ `Read the compressed packed codebase at: ${paths.overview}`,
243
+ "",
244
+ `Produce the outline YAML conforming to the schema above and write it to: ${paths.outline}`,
245
+ "",
246
+ "Do NOT include a `review` field — that section is added by the conversational orchestrator after you finish. Only emit the content fields.",
247
+ "",
248
+ "Do not output YAML in your reply — only write it to the file. When the file is written, end your response with a brief one-line confirmation noting the file path.",
249
+ ].join("\n");
250
+ return { prompt };
251
+ }
252
+ /**
253
+ * Resolve the CLI invocation prefix worker subagents should use when running
254
+ * `cts validate` and `cts style-check` on their partials. Resolved at module
255
+ * load so every dispatch composed in this run uses the same prefix:
256
+ *
257
+ * 1. `DOTREQUIREMENTS_CLI` env var if set (same override the cts-worker
258
+ * hook script uses — keeps the two resolution paths consistent).
259
+ * 2. `node <process.argv[1]>` — points the worker at the exact CLI binary
260
+ * the user just invoked. The worker subagent runs Bash on the same
261
+ * machine in the same project, so reproducing the user's CLI is more
262
+ * reliable than hoping `dotrequirements` on PATH resolves to the same
263
+ * version.
264
+ */
265
+ function resolveCliInvocation() {
266
+ if (process.env.DOTREQUIREMENTS_CLI)
267
+ return process.env.DOTREQUIREMENTS_CLI;
268
+ return `node ${process.argv[1]}`;
269
+ }
270
+ const LOCAL_CLI_INVOCATION = resolveCliInvocation();
271
+ /**
272
+ * Overlay for the specifier dispatch. Inserted between the legacy
273
+ * SPECIFIER_PROMPT and the per-dispatch task instructions. Reinforces:
274
+ * - Customer-grounding discipline (per-area customers from outline.yaml)
275
+ * - source.txt as the read source for area files
276
+ * - Validate + style-check workflow
277
+ */
278
+ const SPECIFIER_OVERLAY = `---
279
+
280
+ ## Updated context — read carefully
281
+
282
+ The system prompt above gives you the discipline for producing a behavioral spec. This dispatch supplies the specific area you're working on, the customer-grounding for that area (from the approved outline), and the validate/style-check commands you must run on your own draft.
283
+
284
+ ## Customer-grounding (the area's customers from the approved outline)
285
+
286
+ The customers below are area-scoped — each describes who interacts with THIS area's behaviors and what they're trying to do at this point in the system. Ground your requirements in these specific customer needs. Use the personas the outline names; don't substitute generic "user" / "developer" framings.
287
+
288
+ ## Output format
289
+
290
+ The PARTIAL FILE is your primary deliverable. Markdown with fenced \`dotrequirements\` blocks per the format rules in the system prompt above. Do NOT include YAML frontmatter, H1 title, summary paragraph, or area H2 — the composer adds those downstream.
291
+
292
+ ## Workflow (legacy CTS-SPEC-3 — REQUIRED)
293
+
294
+ After writing your initial draft, you MUST run validate then style-check on your own partial via Bash. Apply MUST FIX and SHOULD FIX findings via Edit. Style-check runs at most twice. Specific Bash commands appear in the task section below.
295
+ `;
296
+ function composeSpecifier(areaPrefix, options) {
297
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
298
+ const paths = cachePaths(projectRoot);
299
+ if (!existsSync(paths.outline) || !existsSync(paths.source)) {
300
+ return null;
301
+ }
302
+ // Parse and validate the outline. The outline must be approved (or we
303
+ // wouldn't be fanning out specifiers).
304
+ let outline;
305
+ try {
306
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ if (!outline.review || outline.review.result !== "approved") {
312
+ return null;
313
+ }
314
+ const area = outline.areas.find((a) => a.prefix === areaPrefix);
315
+ if (!area)
316
+ return null;
317
+ const partialPath = paths.partial(sanitizeAreaName(area.name));
318
+ const outlineYaml = readFileSync(paths.outline, "utf-8");
319
+ const customersBlock = area.customers
320
+ .map((c, i) => `${i + 1}. ${c.description.trim()}`)
321
+ .join("\n\n");
322
+ const prompt = [
323
+ SPECIFIER_PROMPT,
324
+ "",
325
+ SPECIFIER_OVERLAY,
326
+ "",
327
+ "---",
328
+ "",
329
+ "## Your area",
330
+ "",
331
+ `Name: ${area.name}`,
332
+ `Prefix: ${area.prefix}`,
333
+ `Document defaultPrefix: ${outline.defaultPrefix}`,
334
+ `Requirement ID form: ${outline.defaultPrefix}-${area.prefix}-<N> (sequential, 1-indexed, no zero-padding)`,
335
+ "",
336
+ "### Description (from the approved outline)",
337
+ "",
338
+ area.description.trim(),
339
+ "",
340
+ "### Customers (area-scoped — ground your requirements in these)",
341
+ "",
342
+ customersBlock,
343
+ "",
344
+ "### Source files in this area",
345
+ "",
346
+ ...area.source_files.map((f) => `- ${f}`),
347
+ "",
348
+ `Read these files from the uncompressed pack at ${paths.source} (grep for \`File: <name>\` headers to find each one's content). You can also read them directly from the worktree if you know the paths. You may consult other files in the pack (e.g., tests, READMEs) for behaviors documented outside this area's primary files.`,
349
+ "",
350
+ "---",
351
+ "",
352
+ "## Full outline (for cross-area awareness — what is in scope vs. not)",
353
+ "",
354
+ "```yaml",
355
+ outlineYaml.trim(),
356
+ "```",
357
+ "",
358
+ "---",
359
+ "",
360
+ "## Your task for this dispatch",
361
+ "",
362
+ `Write your partial to: ${partialPath}`,
363
+ "",
364
+ "Then run validate (REQUIRED) and style-check (REQUIRED) on your own partial via Bash:",
365
+ "",
366
+ `- Validate: \`${LOCAL_CLI_INVOCATION} cts validate ${partialPath}\``,
367
+ `- Style-check: \`${LOCAL_CLI_INVOCATION} cts style-check ${partialPath}\``,
368
+ "",
369
+ "Follow the three-phase workflow (Draft → Validate → Style-check) from the system prompt. Apply MUST FIX and SHOULD FIX findings via Edit. Cap style-check at two runs.",
370
+ "",
371
+ 'End with a short confirmation: "Done. Partial saved to <path>."',
372
+ ].join("\n");
373
+ return { prompt };
374
+ }
375
+ const EDITOR_OVERLAY = `---
376
+
377
+ ## Updated context — read carefully
378
+
379
+ The system prompt above describes editing a composed spec **document**. **This dispatch scopes you to ONE partial spec for ONE behavioral area**, not the composed document. The partial is the work-in-progress for one area; CA reviewed it and flagged revisions you must apply.
380
+
381
+ ## Critique shape (different from the legacy reviewer's shape)
382
+
383
+ Our orchestrator uses a simpler critique shape. You receive a list of \`revisions\` — concrete directives to apply. There are NO categorized findings (no coverage_gaps / framing_errors / cross_area_issues / internal_mechanics_drift). Just revisions.
384
+
385
+ You are in **apply-mode-equivalent**: apply each revision in the list as written. Do not introduce changes beyond the listed revisions. Do not restructure unflagged content. If a revision is ambiguous, apply your best literal interpretation and note the ambiguity in your final stdout confirmation.
386
+
387
+ ## Customer-grounding (the area's customers from outline)
388
+
389
+ The customers below are area-scoped. Maintain the customer-grounded framing of existing requirements; any new requirements you add must name the same personas the existing requirements use.
390
+
391
+ ## Workflow (REQUIRED — same as specifier)
392
+
393
+ After applying revisions via Edit, you MUST run validate + style-check on the revised partial via Bash. Apply MUST FIX and SHOULD FIX findings via Edit. Style-check runs at most twice. Specific Bash commands appear in the task section below.
394
+ `;
395
+ function composeEditor(areaPrefix, options) {
396
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
397
+ const paths = cachePaths(projectRoot);
398
+ if (!existsSync(paths.outline)) {
399
+ return null;
400
+ }
401
+ let outline;
402
+ try {
403
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
404
+ }
405
+ catch {
406
+ return null;
407
+ }
408
+ const area = outline.areas.find((a) => a.prefix === areaPrefix);
409
+ if (!area)
410
+ return null;
411
+ // Editor only runs when the area has been reviewed and the latest review
412
+ // thread entry (written by the reviewer into outline.yaml) asks for revisions.
413
+ if (!area.review || area.review.result !== "needs-revision") {
414
+ return null;
415
+ }
416
+ const latestEntry = area.review.thread[area.review.thread.length - 1];
417
+ if (!latestEntry || latestEntry.result !== "needs-revision") {
418
+ return null;
419
+ }
420
+ const partialPath = paths.partial(sanitizeAreaName(area.name));
421
+ if (!existsSync(partialPath)) {
422
+ // Can't edit a partial that doesn't exist yet.
423
+ return null;
424
+ }
425
+ const currentPartial = readFileSync(partialPath, "utf-8");
426
+ const customersBlock = area.customers
427
+ .map((c, i) => `${i + 1}. ${c.description.trim()}`)
428
+ .join("\n\n");
429
+ const revisionsBlock = latestEntry.revisions
430
+ .map((r, i) => `${i + 1}. ${r.trim()}`)
431
+ .join("\n\n");
432
+ const prompt = [
433
+ EDITOR_PROMPT,
434
+ "",
435
+ EDITOR_OVERLAY,
436
+ "",
437
+ "---",
438
+ "",
439
+ "## Your area",
440
+ "",
441
+ `Name: ${area.name}`,
442
+ `Prefix: ${area.prefix}`,
443
+ `Document defaultPrefix: ${outline.defaultPrefix}`,
444
+ `Requirement ID form: ${outline.defaultPrefix}-${area.prefix}-<N>`,
445
+ "",
446
+ "### Description (from the approved outline)",
447
+ "",
448
+ area.description.trim(),
449
+ "",
450
+ "### Customers (area-scoped — preserve their grounding in your edits)",
451
+ "",
452
+ customersBlock,
453
+ "",
454
+ "---",
455
+ "",
456
+ "## Current partial (read via Read tool to load it into your view; the content is also embedded here for reference)",
457
+ "",
458
+ `Path: ${partialPath}`,
459
+ "",
460
+ "```markdown",
461
+ currentPartial.trim(),
462
+ "```",
463
+ "",
464
+ "---",
465
+ "",
466
+ "## Reviewer's revisions (apply each as written)",
467
+ "",
468
+ revisionsBlock,
469
+ "",
470
+ "---",
471
+ "",
472
+ "## Your task for this dispatch",
473
+ "",
474
+ `Use the Edit tool to apply each revision to the partial at ${partialPath}. Do not introduce changes beyond the listed revisions.`,
475
+ "",
476
+ "Then run validate (REQUIRED) and style-check (REQUIRED) on the revised partial via Bash:",
477
+ "",
478
+ `- Validate: \`${LOCAL_CLI_INVOCATION} cts validate ${partialPath}\``,
479
+ `- Style-check: \`${LOCAL_CLI_INVOCATION} cts style-check ${partialPath}\``,
480
+ "",
481
+ "Apply MUST FIX and SHOULD FIX findings from style-check via Edit. Cap style-check at two runs.",
482
+ "",
483
+ 'End with a brief stdout confirmation summarizing the kinds of changes you made (e.g., "Applied 3 revisions: added 2 requirements about edge cases, rephrased 1 framing-error finding.").',
484
+ ].join("\n");
485
+ return { prompt };
486
+ }
487
+ const REVIEWER_PROMPT = `You are a codebase-to-spec **reviewer**. A specifier (or editor) worker has drafted the behavioral spec for ONE area; you are a fresh, independent pair of eyes reviewing that draft before it is accepted. You did not write it — judge it on its merits.
488
+
489
+ ## What you are reviewing
490
+
491
+ A "partial" — a Markdown file of fenced \`dotrequirements\` blocks capturing the behavior of one area, grounded in that area's customers. The schema has already been validated by the author; judge substance, not syntax.
492
+
493
+ ## Review criteria (priority order)
494
+
495
+ 1. **Customer-grounding** — Each requirement is framed around the area's named customers (the personas below), describing what that customer observes. Generic "the user" / "the developer" framing, or framing around the code's internals instead of the customer, is a problem.
496
+ 2. **Behavioral framing** — Requirements describe observable behavior, not API signatures, function names, data shapes, or implementation mechanics.
497
+ 3. **Independent testability** — Each requirement reads on its own. Cross-references between requirements ("as in REQ-X"), or criteria bundling several independent actions, hurt testability.
498
+ 4. **Coverage** — Behaviors visible in the area's source files are captured; flag notable missing behaviors.
499
+ 5. **No internal-mechanics leakage** — Internal vocabulary, private helpers, or pipeline jargon a customer would never observe should not appear.
500
+
501
+ ## Your verdict
502
+
503
+ - **approved** — the draft meets the criteria well enough to accept. No revisions.
504
+ - **needs-revision** — one or more criteria are not met. Provide **concrete, actionable** revision directives — each says *what to change*, specific enough that an editor can apply it mechanically. Not "this feels off" but "Reframe CTSPROMPT-STYLE-2 around what Riley observes from the checker, not the checker's internal categories."
505
+
506
+ Be a real reviewer: approve genuinely good drafts (don't invent problems), but don't rubber-stamp drafts with real customer-grounding, framing, or testability issues.`;
507
+ function composeReviewer(areaPrefix, options) {
508
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
509
+ const paths = cachePaths(projectRoot);
510
+ if (!existsSync(paths.outline)) {
511
+ return null;
512
+ }
513
+ let outline;
514
+ try {
515
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
516
+ }
517
+ catch {
518
+ return null;
519
+ }
520
+ // Same gate as composeSpecifier: area reviews only run against areas of an
521
+ // approved outline, so an unapproved decomposition never gets rubber-stamped
522
+ // area by area.
523
+ if (!outline.review || outline.review.result !== "approved") {
524
+ return null;
525
+ }
526
+ const area = outline.areas.find((a) => a.prefix === areaPrefix);
527
+ if (!area)
528
+ return null;
529
+ const partialPath = paths.partial(sanitizeAreaName(area.name));
530
+ if (!existsSync(partialPath)) {
531
+ // Nothing to review until the specifier has produced a partial.
532
+ return null;
533
+ }
534
+ const currentPartial = readFileSync(partialPath, "utf-8");
535
+ const customersBlock = area.customers
536
+ .map((c, i) => `${i + 1}. ${c.description.trim()}`)
537
+ .join("\n\n");
538
+ const prompt = [
539
+ REVIEWER_PROMPT,
540
+ "",
541
+ "---",
542
+ "",
543
+ "## Your area",
544
+ "",
545
+ `Name: ${area.name}`,
546
+ `Prefix: ${area.prefix}`,
547
+ "",
548
+ "### Description (from the approved outline)",
549
+ "",
550
+ area.description.trim(),
551
+ "",
552
+ "### Customers (area-scoped — requirements must be grounded in these)",
553
+ "",
554
+ customersBlock,
555
+ "",
556
+ "---",
557
+ "",
558
+ "## The draft partial to review",
559
+ "",
560
+ `Path: ${partialPath}`,
561
+ "",
562
+ "```markdown",
563
+ currentPartial.trim(),
564
+ "```",
565
+ "",
566
+ "---",
567
+ "",
568
+ "## Your task for this dispatch",
569
+ "",
570
+ "1. Review the draft against the criteria above and decide your verdict.",
571
+ `2. Record your verdict in the outline at: ${paths.outline}`,
572
+ ` Find the area whose \`prefix:\` is \`${areaPrefix}\` and append one entry to its`,
573
+ " `review.thread` (create the area's `review:` block with a single-entry thread if it",
574
+ " has none yet), and set that area's `review.result` to match. Entry shapes:",
575
+ " - `{ result: approved }`",
576
+ ' - `{ result: needs-revision, revisions: ["<directive>", "..."] }`',
577
+ " Use the **Edit** tool and preserve every other part of the file exactly. Do NOT",
578
+ " rewrite the whole file. If the Edit fails because the file changed since you read it",
579
+ " (another area's reviewer wrote concurrently), re-read the outline and re-apply your",
580
+ " edit to the current content.",
581
+ "3. Return the same verdict as your structured output:",
582
+ ' `{ "result": "approved" | "needs-revision", "revisions": [...] }`.',
583
+ "",
584
+ "Your outline edit and your structured return MUST carry the same verdict.",
585
+ ].join("\n");
586
+ return { prompt };
587
+ }
588
+ const OUTLINE_REVIEWER_PROMPT = `You are a codebase-to-spec **outline reviewer**. A planner produced an outline that carves a packed codebase into behavioral areas, each with its own area-scoped customers. You are an independent reviewer (not the planner) judging whether this decomposition is good enough to fan out specifiers against.
589
+
590
+ ## Criteria
591
+
592
+ 1. **Area granularity** — each area is a coherent behavioral surface: not so broad it smears several distinct behaviors together, not so fine it is a trivial sliver.
593
+ 2. **Area boundaries** — areas do not overlap; a given behavior belongs to exactly one area.
594
+ 3. **Coverage** — the substantive behavioral files in the pack are each assigned to some area. Non-behavioral files (build config, pure test scaffolding) may be omitted.
595
+ 4. **Customer-vocabulary names** — area names read in the customer's language (what the software does for someone), not architectural or file-layout labels. Each area declares at least one area-scoped customer with a concrete description.
596
+ 5. **Prefixes** — each area has a distinct uppercase prefix.
597
+
598
+ ## Verdict
599
+
600
+ - **approved** — the decomposition is good enough to fan out.
601
+ - **needs-revision** — give concrete, actionable directives the planner can apply (e.g. "split the FOO area into producer and reviewer behaviors, each its own area"; "rename BAR to customer vocabulary"; "assign baz.ts to an area"). Not "this feels off."
602
+
603
+ Be a real reviewer: approve good decompositions, but flag real granularity / boundary / coverage / naming problems.`;
604
+ function composeOutlineReviewer(options) {
605
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
606
+ const paths = cachePaths(projectRoot);
607
+ if (!existsSync(paths.outline)) {
608
+ return null;
609
+ }
610
+ // Parse to confirm it is a well-formed outline before reviewing.
611
+ try {
612
+ parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
613
+ }
614
+ catch {
615
+ return null;
616
+ }
617
+ const outlineYaml = readFileSync(paths.outline, "utf-8");
618
+ const prompt = [
619
+ OUTLINE_REVIEWER_PROMPT,
620
+ "",
621
+ "---",
622
+ "",
623
+ "## The outline to review",
624
+ "",
625
+ `Path: ${paths.outline}`,
626
+ "",
627
+ "```yaml",
628
+ outlineYaml.trim(),
629
+ "```",
630
+ "",
631
+ "---",
632
+ "",
633
+ "## Your task for this dispatch",
634
+ "",
635
+ "1. Review the outline against the criteria above and decide your verdict.",
636
+ `2. Record your verdict in the outline at: ${paths.outline}`,
637
+ " Append one entry to the document-level (top-level) `review.thread` — a sibling of",
638
+ " `title` / `areas`, NOT an area's `review` — creating the top-level `review:` block",
639
+ " with a single-entry thread if it has none yet, and set the top-level `review.result`",
640
+ " to match. Entry shapes:",
641
+ " - `{ result: approved }`",
642
+ ' - `{ result: needs-revision, revisions: ["<directive>", "..."] }`',
643
+ " Use the **Edit** tool and preserve every other part of the file exactly. If the Edit",
644
+ " fails because the file changed since you read it, re-read and re-apply.",
645
+ "3. Return the same verdict as your structured output:",
646
+ ' `{ "result": "approved" | "needs-revision", "revisions": [...] }`.',
647
+ "",
648
+ "Your outline edit and your structured return MUST carry the same verdict.",
649
+ ].join("\n");
650
+ return { prompt };
651
+ }
652
+ const CROSS_AREA_REVIEWER_OVERLAY = `---
653
+
654
+ ## Orchestrator adaptations — read carefully
655
+
656
+ You are reviewing the **composed spec** after per-area specifiers converged and their partials were assembled into one document. Focus on the document-level / cross-area pass — what only a whole-document view can catch: duplication across areas, inconsistent terminology or personas, awkward cross-cutting splits, depth imbalance, and document-level coverage gaps or framing errors that span areas. Per-area requirement quality has already been reviewed; do not re-litigate within-area style.
657
+
658
+ Run the **persona roster check** (cheap to fix here, impossible to see per-area): each distinct customer carries exactly ONE name across the whole document, and one name never denotes different customers in different areas. Flag confusably similar names.
659
+
660
+ ## Verdict shape (simpler than the legacy reviewer's)
661
+
662
+ Ignore the legacy categorized-findings object and three-state verdict described above. Return ONLY this shape:
663
+
664
+ - \`{ "result": "approved", "revisions": [] }\` — the composed spec is coherent and ready as-is. Reserve for genuinely good specs.
665
+ - \`{ "result": "needs-revision", "revisions": ["<directive>", "..."] }\` — fold every change you want (whether you'd have called it approved-with-revisions or requires-another-review) into a flat list of concrete, actionable directives an editor can apply mechanically. Cite requirement IDs and area names. Not "this feels off."`;
666
+ function composeCrossAreaReviewer(options) {
667
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
668
+ const paths = cachePaths(projectRoot);
669
+ if (!existsSync(paths.outline) || !existsSync(paths.composedSpec)) {
670
+ // Need both the outline (to record the verdict) and the composed spec (to
671
+ // review). The latter only exists once the partials have been composed.
672
+ return null;
673
+ }
674
+ const composed = readFileSync(paths.composedSpec, "utf-8");
675
+ const prompt = [
676
+ SPEC_REVIEWER_PROMPT,
677
+ "",
678
+ CROSS_AREA_REVIEWER_OVERLAY,
679
+ "",
680
+ "---",
681
+ "",
682
+ "## The composed spec to review",
683
+ "",
684
+ `Path: ${paths.composedSpec}`,
685
+ "",
686
+ "```markdown",
687
+ composed.trim(),
688
+ "```",
689
+ "",
690
+ "---",
691
+ "",
692
+ `For codebase grounding, the uncompressed pack is at ${paths.source} (grep for \`File: <name>\` headers). Consult it to spot-check coverage and framing against the actual code; you do not need to read all of it.`,
693
+ "",
694
+ "---",
695
+ "",
696
+ "## Your task for this dispatch",
697
+ "",
698
+ "1. Review the composed spec against the criteria above and decide your verdict.",
699
+ `2. Record your verdict in the outline at: ${paths.outline}`,
700
+ " Append one entry to the document-level (top-level) `crossAreaReview.thread` — a",
701
+ " sibling of `title` / `summary` / `areas` / `review`. This is SEPARATE from the",
702
+ " top-level `review` (which records outline approval); do NOT touch `review`. Create",
703
+ " the top-level `crossAreaReview:` block with a single-entry thread if it has none",
704
+ " yet, and set its `crossAreaReview.result` to match. Entry shapes:",
705
+ " - `{ result: approved }`",
706
+ ' - `{ result: needs-revision, revisions: ["<directive>", "..."] }`',
707
+ " Use the **Edit** tool and preserve every other part of the file exactly. If the Edit",
708
+ " fails because the file changed since you read it, re-read and re-apply.",
709
+ "3. Return the same verdict as your structured output:",
710
+ ' `{ "result": "approved" | "needs-revision", "revisions": [...] }`.',
711
+ "",
712
+ "Your outline edit and your structured return MUST carry the same verdict.",
713
+ ].join("\n");
714
+ return { prompt };
715
+ }
716
+ const COMPOSE_EDITOR_OVERLAY = `---
717
+
718
+ ## Critique shape (different from the legacy reviewer's shape)
719
+
720
+ You receive a flat list of \`revisions\` — concrete directives to apply to the composed spec. There are NO categorized findings (no coverage_gaps / framing_errors / cross_area_issues / internal_mechanics_drift). Just the revisions listed below.
721
+
722
+ Apply each revision as written; do not introduce changes beyond the listed revisions, and do not restructure unflagged content. If a revision is ambiguous, apply your best literal interpretation and note it in your final stdout confirmation. Keep the document coherent — this IS the composed document (not a single area's partial).`;
723
+ function composeComposeEditor(options) {
724
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
725
+ const paths = cachePaths(projectRoot);
726
+ if (!existsSync(paths.outline) || !existsSync(paths.composedSpec)) {
727
+ // Need the outline (for the cross-area revisions) and the composed spec
728
+ // (to edit). Either missing → nothing to do.
729
+ return null;
730
+ }
731
+ let outline;
732
+ try {
733
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
734
+ }
735
+ catch {
736
+ return null;
737
+ }
738
+ // Editor only runs when the cross-area reviewer recorded a needs-revision
739
+ // verdict in the top-level crossAreaReview thread (mirrors the per-area
740
+ // editor's gate on area.review).
741
+ if (!outline.crossAreaReview ||
742
+ outline.crossAreaReview.result !== "needs-revision") {
743
+ return null;
744
+ }
745
+ const latestEntry = outline.crossAreaReview.thread[outline.crossAreaReview.thread.length - 1];
746
+ if (!latestEntry || latestEntry.result !== "needs-revision") {
747
+ return null;
748
+ }
749
+ const composed = readFileSync(paths.composedSpec, "utf-8");
750
+ const revisionsBlock = latestEntry.revisions
751
+ .map((r, i) => `${i + 1}. ${r.trim()}`)
752
+ .join("\n\n");
753
+ const prompt = [
754
+ EDITOR_PROMPT,
755
+ "",
756
+ COMPOSE_EDITOR_OVERLAY,
757
+ "",
758
+ "---",
759
+ "",
760
+ "## The composed spec to edit",
761
+ "",
762
+ `Path: ${paths.composedSpec}`,
763
+ "",
764
+ "```markdown",
765
+ composed.trim(),
766
+ "```",
767
+ "",
768
+ "---",
769
+ "",
770
+ "## Cross-area revisions (apply each as written)",
771
+ "",
772
+ revisionsBlock,
773
+ "",
774
+ "---",
775
+ "",
776
+ "## Your task for this dispatch",
777
+ "",
778
+ `Use the Edit tool to apply each revision above to the composed spec at ${paths.composedSpec}. Do not introduce changes beyond the listed revisions.`,
779
+ "",
780
+ `Then run validate (REQUIRED) on the composed spec via Bash: \`${LOCAL_CLI_INVOCATION} cts validate ${paths.composedSpec}\``,
781
+ "",
782
+ "Fix any schema errors validate reports. Do NOT run style-check here: the per-area partials were already style-checked before composition, and cross-area edits are structural (dedup, terminology, persona consistency), so re-style-checking the whole assembled document is unnecessary.",
783
+ "",
784
+ "End with a brief stdout confirmation summarizing the kinds of changes you made.",
785
+ ].join("\n");
786
+ return { prompt };
787
+ }
788
+ function composePlannerRevise(turn, options) {
789
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
790
+ const paths = cachePaths(projectRoot);
791
+ if (!existsSync(paths.overview) || !existsSync(paths.outline)) {
792
+ return null;
793
+ }
794
+ // Read and validate the current outline. The latest thread entry must
795
+ // be `needs-revision` for a revise dispatch to make sense.
796
+ let outline;
797
+ try {
798
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
799
+ }
800
+ catch {
801
+ return null;
802
+ }
803
+ if (!outline.review || outline.review.result !== "needs-revision") {
804
+ return null;
805
+ }
806
+ const latestEntry = outline.review.thread[outline.review.thread.length - 1];
807
+ if (!latestEntry || latestEntry.result !== "needs-revision") {
808
+ return null;
809
+ }
810
+ const priorOutlineYaml = readFileSync(paths.outline, "utf-8");
811
+ const revisionsYaml = latestEntry.revisions
812
+ .map((r, i) => `- # revision ${i + 1}\n ${r.split("\n").join("\n ")}`)
813
+ .join("\n");
814
+ const prompt = [
815
+ PLANNER_REVISE_PROMPT,
816
+ "",
817
+ PLANNER_YAML_OVERLAY,
818
+ "",
819
+ "---",
820
+ "",
821
+ "## Inputs for this revision",
822
+ "",
823
+ `### Compressed packed codebase`,
824
+ `Path: ${paths.overview}`,
825
+ "",
826
+ `### Current outline.yaml (turn ${turn - 1})`,
827
+ "```yaml",
828
+ priorOutlineYaml.trim(),
829
+ "```",
830
+ "",
831
+ `### Reviewer's revisions (the latest \`review.thread\` entry's revisions, extracted)`,
832
+ "```yaml",
833
+ revisionsYaml,
834
+ "```",
835
+ "",
836
+ "---",
837
+ "",
838
+ "## Your task for this dispatch",
839
+ "",
840
+ "Apply each revision in the list as written. Preserve areas that the revisions don't touch.",
841
+ "",
842
+ "**IMPORTANT: Preserve the `review` section from the current outline verbatim.** Copy `review.result` and the entire `review.thread` array into your output exactly as they appear in the current outline. The orchestrator manages the review section; you must not modify it. After you write the file, the orchestrator will append a new review entry.",
843
+ "",
844
+ `Write the revised outline YAML to: ${paths.outline}`,
845
+ "",
846
+ "Do not output YAML in your reply — only write it to the file. End with a brief one-line confirmation noting the file path.",
847
+ ].join("\n");
848
+ return { prompt };
849
+ }
850
+ //# sourceMappingURL=dispatch.js.map