@popoverai/dotrequirements 0.24.3 → 0.26.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 (75) hide show
  1. package/README.md +7 -8
  2. package/dist/cli.js +8 -1
  3. package/dist/codebase-to-spec/dispatch.d.ts +60 -14
  4. package/dist/codebase-to-spec/dispatch.js +381 -15
  5. package/dist/codebase-to-spec/pack.d.ts +7 -0
  6. package/dist/codebase-to-spec/pack.js +29 -8
  7. package/dist/codebase-to-spec/present.d.ts +9 -0
  8. package/dist/codebase-to-spec/present.js +23 -2
  9. package/dist/codebase-to-spec/prompts/editor.d.ts +1 -1
  10. package/dist/codebase-to-spec/prompts/editor.js +1 -1
  11. package/dist/codebase-to-spec/prompts/specifier.d.ts +1 -1
  12. package/dist/codebase-to-spec/prompts/specifier.js +3 -2
  13. package/dist/codebase-to-spec/schemas.d.ts +153 -0
  14. package/dist/codebase-to-spec/schemas.js +111 -0
  15. package/dist/codebase-to-spec/skill-install.d.ts +42 -29
  16. package/dist/codebase-to-spec/skill-install.js +122 -112
  17. package/dist/codebase-to-spec/version-check.d.ts +31 -0
  18. package/dist/codebase-to-spec/version-check.js +56 -0
  19. package/dist/commands/ai-setup.d.ts +12 -1
  20. package/dist/commands/ai-setup.js +65 -33
  21. package/dist/commands/codebase-to-spec/dispatch-context.d.ts +2 -5
  22. package/dist/commands/codebase-to-spec/dispatch-context.js +2 -5
  23. package/dist/commands/codebase-to-spec/dispatch-editor.d.ts +0 -1
  24. package/dist/commands/codebase-to-spec/dispatch-editor.js +0 -1
  25. package/dist/commands/codebase-to-spec/dispatch-planner.d.ts +0 -1
  26. package/dist/commands/codebase-to-spec/dispatch-planner.js +0 -1
  27. package/dist/commands/codebase-to-spec/dispatch-spec.d.ts +3 -6
  28. package/dist/commands/codebase-to-spec/dispatch-spec.js +3 -6
  29. package/dist/commands/codebase-to-spec/index.js +3 -2
  30. package/dist/commands/codebase-to-spec/pack.d.ts +9 -0
  31. package/dist/commands/codebase-to-spec/pack.js +23 -3
  32. package/dist/commands/codebase-to-spec/skill-install.js +2 -9
  33. package/dist/commands/init.js +6 -1
  34. package/dist/commands/link-resolution.d.ts +79 -0
  35. package/dist/commands/link-resolution.js +141 -0
  36. package/dist/commands/link.d.ts +14 -4
  37. package/dist/commands/link.js +369 -16
  38. package/dist/commands/pull.js +19 -2
  39. package/dist/commands/push.js +36 -2
  40. package/dist/convex.d.ts +5 -3
  41. package/dist/convex.js +5 -3
  42. package/dist/harness/cache.d.ts +0 -14
  43. package/dist/harness/cache.js +1 -41
  44. package/dist/harness/finalize.js +2 -2
  45. package/dist/harness/prepare.js +1 -3
  46. package/dist/harness/requirementsLoader.d.ts +3 -3
  47. package/dist/harness/requirementsLoader.js +13 -8
  48. package/dist/mcp/handlers/authoring.d.ts +5 -5
  49. package/dist/mcp/handlers/authoring.js +9 -9
  50. package/dist/mcp/handlers/push.d.ts +2 -2
  51. package/dist/mcp/handlers/push.js +36 -3
  52. package/dist/mcp/handlers/review.d.ts +4 -4
  53. package/dist/mcp/handlers/review.js +4 -4
  54. package/dist/mcp/handlers/search.d.ts +1 -1
  55. package/dist/mcp/handlers/search.js +1 -1
  56. package/dist/mcp/index.js +29 -0
  57. package/dist/push/core.d.ts +18 -0
  58. package/dist/push/core.js +70 -3
  59. package/dist/push/index.d.ts +1 -1
  60. package/dist/push/index.js +1 -1
  61. package/dist/schema/parser-core.js +5 -1
  62. package/dist/schema/parser.js +5 -1
  63. package/dist/schema/run-marker.d.ts +38 -0
  64. package/dist/schema/run-marker.js +138 -0
  65. package/dist/schema/schemas.d.ts +12 -0
  66. package/dist/schema/schemas.js +1 -0
  67. package/dist/templates/agents/cts-worker.md +3 -3
  68. package/dist/templates/skills/codebase-to-spec/SKILL.md +56 -158
  69. package/dist/templates/workflows/specify-codebase.js +374 -0
  70. package/dist/utils/own-package.d.ts +10 -0
  71. package/dist/utils/own-package.js +13 -0
  72. package/dist/utils/project-selector.d.ts +5 -0
  73. package/dist/utils/project-selector.js +4 -0
  74. package/package.json +3 -3
  75. package/dist/templates/hooks/cts-worker-persona.sh +0 -76
@@ -0,0 +1,374 @@
1
+ export const meta = {
2
+ name: 'specify-codebase',
3
+ description:
4
+ 'Codebase-to-spec: autonomously plan a behavioral outline, fan out specifiers across its areas, and converge each area via independent review + editor passes — no mid-run human input',
5
+ whenToUse:
6
+ 'Invoked by the codebase-to-spec skill after `cts pack`. Not run directly.',
7
+ phases: [
8
+ { title: 'Outline' },
9
+ { title: 'Specify' },
10
+ { title: 'Converge' },
11
+ { title: 'Reconcile' },
12
+ ],
13
+ }
14
+
15
+ // Injected globals: agent, pipeline, parallel, phase, log, args.
16
+ //
17
+ // args:
18
+ // cli — resolved dotrequirements CLI invocation workers shell out to
19
+ // (e.g. "dotrequirements" or "node /abs/path/packages/cli/dist/cli.js")
20
+ // roundsCap — max review→edit rounds per area before giving up (default 5)
21
+ // outlineRoundsCap — max plan→review→revise rounds for the outline (default 4)
22
+ // crossAreaRoundsCap — max cross-area review→edit rounds on the composed spec (default 5)
23
+ //
24
+ // The caps are a runaway backstop, not a target: an area stops the moment its
25
+ // reviewer approves, and reports `unconverged` if it can't converge in N rounds.
26
+ // Defaults are set high enough to give careful review room on messy codebases.
27
+ //
28
+ // args may arrive as a JSON string depending on how the caller passes it; normalize.
29
+ const input = typeof args === 'string' ? JSON.parse(args) : args || {}
30
+ // `cli` defaults to the invocation stamped at install time (a version-pinned
31
+ // `npx` form for installed users, so no PATH binary is needed and every
32
+ // shell-out in a run resolves the same version); the dev repo passes a
33
+ // `node <repo>/dist/cli.js` invocation explicitly.
34
+ const {
35
+ cli = '{{DOTREQ_CLI}}',
36
+ roundsCap = 5,
37
+ outlineRoundsCap = 4,
38
+ crossAreaRoundsCap = 5,
39
+ } = input
40
+
41
+ // --- Schemas (mirrors of the JSON Schemas in schemas.ts; a sync test guards drift) ---
42
+
43
+ const PARTIAL_RESULT_SCHEMA = {
44
+ type: 'object',
45
+ properties: {
46
+ area_prefix: { type: 'string' },
47
+ partial_path: { type: 'string' },
48
+ status: { type: 'string', enum: ['drafted', 'skipped-resume'] },
49
+ requirement_count: { type: 'integer', minimum: 0 },
50
+ },
51
+ required: ['area_prefix', 'partial_path', 'status'],
52
+ additionalProperties: false,
53
+ }
54
+
55
+ const REVIEW_VERDICT_SCHEMA = {
56
+ type: 'object',
57
+ properties: {
58
+ result: { type: 'string', enum: ['approved', 'needs-revision'] },
59
+ revisions: { type: 'array', items: { type: 'string' } },
60
+ },
61
+ required: ['result'],
62
+ additionalProperties: false,
63
+ }
64
+
65
+ // Shape `cts dispatch-spec` emits (one entry per approved-outline area).
66
+ const AREAS_SCHEMA = {
67
+ type: 'object',
68
+ properties: {
69
+ areas: {
70
+ type: 'array',
71
+ items: {
72
+ type: 'object',
73
+ properties: {
74
+ dispatch_id: { type: 'string' },
75
+ area_prefix: { type: 'string' },
76
+ area_name: { type: 'string' },
77
+ output_path: { type: 'string' },
78
+ },
79
+ required: ['dispatch_id', 'area_prefix', 'area_name', 'output_path'],
80
+ additionalProperties: false,
81
+ },
82
+ },
83
+ },
84
+ required: ['areas'],
85
+ additionalProperties: false,
86
+ }
87
+
88
+ // Thin self-compose wrapper: a worker fetches its full instructions from the CLI.
89
+ function follow(line) {
90
+ return [
91
+ `Run exactly this command: ${line}`,
92
+ `It prints JSON of the form {"prompt": "..."}. Read the "prompt" field and follow it EXACTLY.`,
93
+ ]
94
+ }
95
+
96
+ // ============================ Stage 0: outline ============================
97
+ // plan → review → revise → re-review, until the outline reviewer approves the
98
+ // area decomposition or the cap is hit. Sequential (one outline), so the
99
+ // reviewer's writes to the document-level review.thread never contend.
100
+ phase('Outline')
101
+
102
+ await agent(
103
+ [
104
+ `You are a codebase-to-spec planner. Produce the behavioral-area outline for the packed codebase.`,
105
+ ...follow(`${cli} cts dispatch-context planner-initial`),
106
+ `End with a one-line confirmation of the outline path you wrote.`,
107
+ ].join('\n'),
108
+ { agentType: 'cts-worker', label: 'plan outline', phase: 'Outline' },
109
+ )
110
+
111
+ let outlineApproved = false
112
+ for (let round = 1; round <= outlineRoundsCap; round++) {
113
+ const verdict = await agent(
114
+ [
115
+ `You are a codebase-to-spec outline reviewer (independent of the planner).`,
116
+ ...follow(`${cli} cts dispatch-context outline-reviewer`),
117
+ `It tells you to record your verdict in the outline's top-level review thread and return it.`,
118
+ `Return { "result": "approved" | "needs-revision", "revisions": [...] }.`,
119
+ ].join('\n'),
120
+ {
121
+ schema: REVIEW_VERDICT_SCHEMA,
122
+ agentType: 'cts-worker',
123
+ label: `review outline r${round}`,
124
+ phase: 'Outline',
125
+ },
126
+ )
127
+
128
+ // Fail-closed: only an explicit "approved" verdict advances. A null verdict
129
+ // (reviewer subagent died after retries) is treated as non-approval — fall
130
+ // through to revise/re-review so we never fan out an unreviewed outline.
131
+ if (verdict?.result === 'approved') {
132
+ outlineApproved = true
133
+ break
134
+ }
135
+ if (round === outlineRoundsCap) break
136
+
137
+ await agent(
138
+ [
139
+ `You are a codebase-to-spec planner applying the reviewer's outline revisions.`,
140
+ ...follow(`${cli} cts dispatch-context planner-revise-${round + 1}`),
141
+ `End with a one-line confirmation.`,
142
+ ].join('\n'),
143
+ { agentType: 'cts-worker', label: `revise outline r${round}`, phase: 'Outline' },
144
+ )
145
+ }
146
+
147
+ if (!outlineApproved) {
148
+ // No mid-run HITL: don't fan out a decomposition the reviewer wouldn't pass.
149
+ log(`outline did not converge within ${outlineRoundsCap} rounds`)
150
+ return {
151
+ status: 'outline-unconverged',
152
+ outline_rounds: outlineRoundsCap,
153
+ areas: [],
154
+ converged_count: 0,
155
+ unconverged: [],
156
+ failed: [],
157
+ total: 0,
158
+ }
159
+ }
160
+
161
+ // ===================== Stage 1+2: fan out + converge =====================
162
+ // Enumerate the approved outline's areas (an agent runs the CLI; the script
163
+ // can't), then run each area through specify → review/edit independently.
164
+ phase('Specify')
165
+
166
+ const enumerated = await agent(
167
+ [
168
+ `Run exactly this command: ${cli} cts dispatch-spec`,
169
+ `It prints a JSON array, one entry per area of the approved outline.`,
170
+ `Return { "areas": <that array, verbatim> }.`,
171
+ ].join('\n'),
172
+ { schema: AREAS_SCHEMA, agentType: 'cts-worker', label: 'enumerate areas', phase: 'Specify' },
173
+ )
174
+
175
+ // Fail-closed: a dead enumerate agent (null return) must surface as a failure,
176
+ // not read as an empty-but-successful decomposition that sails through fan-out
177
+ // and reconcile to a hollow `done`.
178
+ if (!enumerated) {
179
+ log('area enumeration failed — cannot fan out')
180
+ return {
181
+ status: 'enumerate-failed',
182
+ areas: [],
183
+ converged_count: 0,
184
+ unconverged: [],
185
+ failed: [],
186
+ total: 0,
187
+ }
188
+ }
189
+ const areas = enumerated.areas
190
+
191
+ async function specify(area) {
192
+ return agent(
193
+ [
194
+ `You are a codebase-to-spec specifier worker for the area "${area.area_name}" (prefix ${area.area_prefix}).`,
195
+ ``,
196
+ `Step 1 — Resume check. If a non-empty partial already exists at:`,
197
+ ` ${area.output_path}`,
198
+ ` then do NOT regenerate it. Return`,
199
+ ` { "area_prefix": "${area.area_prefix}", "partial_path": "${area.output_path}", "status": "skipped-resume" }.`,
200
+ ``,
201
+ `Step 2 — Otherwise, run exactly this command:`,
202
+ ` ${cli} cts dispatch-context specifier-${area.area_prefix}`,
203
+ ` Read the "prompt" field of its JSON output and follow it EXACTLY — which source files to`,
204
+ ` read, how to ground requirements in this area's customers, where to write the partial,`,
205
+ ` and how to validate + style-check it.`,
206
+ ``,
207
+ `Step 3 — Return`,
208
+ ` { "area_prefix": "${area.area_prefix}", "partial_path": "${area.output_path}",`,
209
+ ` "status": "drafted", "requirement_count": <number of requirements you wrote> }.`,
210
+ ].join('\n'),
211
+ {
212
+ schema: PARTIAL_RESULT_SCHEMA,
213
+ agentType: 'cts-worker',
214
+ label: `specify ${area.area_name}`,
215
+ phase: 'Specify',
216
+ },
217
+ )
218
+ }
219
+
220
+ async function reviewEditLoop(specifyResult, area) {
221
+ // Fail-closed (CTSO-INTEG-3.2): a dead specifier (null return) means there may
222
+ // be no partial to review — record the failure and continue with the other
223
+ // areas, instead of burning review/edit rounds on a draft that may not exist.
224
+ if (!specifyResult) {
225
+ log(`area ${area.area_prefix}: specifier failed — recording and moving on`)
226
+ return {
227
+ area_prefix: area.area_prefix,
228
+ area_name: area.area_name,
229
+ partial_path: area.output_path,
230
+ status: 'failed',
231
+ rounds: 0,
232
+ }
233
+ }
234
+
235
+ for (let round = 1; round <= roundsCap; round++) {
236
+ const verdict = await agent(
237
+ [
238
+ `You are a codebase-to-spec reviewer for the area "${area.area_name}" (prefix ${area.area_prefix}).`,
239
+ ...follow(`${cli} cts dispatch-context reviewer-${area.area_prefix}`),
240
+ `It tells you to review the partial, record your verdict in the outline, and return it.`,
241
+ `Return { "result": "approved" | "needs-revision", "revisions": [...] }.`,
242
+ ].join('\n'),
243
+ {
244
+ schema: REVIEW_VERDICT_SCHEMA,
245
+ agentType: 'cts-worker',
246
+ label: `review ${area.area_name} r${round}`,
247
+ phase: 'Converge',
248
+ },
249
+ )
250
+
251
+ if (verdict?.result === 'approved') {
252
+ return {
253
+ area_prefix: area.area_prefix,
254
+ area_name: area.area_name,
255
+ partial_path: area.output_path,
256
+ status: 'converged',
257
+ rounds: round - 1,
258
+ }
259
+ }
260
+
261
+ // Fail-closed: a null verdict (dead reviewer) is NOT approval — but it also
262
+ // recorded no fresh revisions in the outline, so dispatching an editor here
263
+ // would only re-apply the previous round's already-applied list (or have
264
+ // nothing to act on in round 1). Skip straight to the next review round.
265
+ if (!verdict) continue
266
+
267
+ // Mirror the outline and cross-area loops: the final round's verdict is
268
+ // final. Without this guard a last editor pass would run whose output is
269
+ // never re-reviewed before composition.
270
+ if (round === roundsCap) break
271
+
272
+ await agent(
273
+ [
274
+ `You are a codebase-to-spec editor for the area "${area.area_name}" (prefix ${area.area_prefix}).`,
275
+ ...follow(`${cli} cts dispatch-context editor-${area.area_prefix}`),
276
+ `It embeds the reviewer's revisions and tells you to apply them, then validate + style-check.`,
277
+ `Return { "area_prefix": "${area.area_prefix}", "partial_path": "${area.output_path}",`,
278
+ ` "status": "drafted", "requirement_count": <number of requirements now in the partial> }.`,
279
+ ].join('\n'),
280
+ {
281
+ schema: PARTIAL_RESULT_SCHEMA,
282
+ agentType: 'cts-worker',
283
+ label: `edit ${area.area_name} r${round}`,
284
+ phase: 'Converge',
285
+ },
286
+ )
287
+ }
288
+
289
+ log(`area ${area.area_prefix} hit roundsCap=${roundsCap} without approval`)
290
+ return {
291
+ area_prefix: area.area_prefix,
292
+ area_name: area.area_name,
293
+ partial_path: area.output_path,
294
+ status: 'unconverged',
295
+ rounds: roundsCap,
296
+ }
297
+ }
298
+
299
+ log(`Outline approved — fanning out ${areas.length} area(s)`)
300
+
301
+ const results = (await pipeline(areas, specify, reviewEditLoop)).filter(Boolean)
302
+
303
+ // ===================== Stage 3: compose + cross-area reconcile =====================
304
+ // The per-area partials have converged. Assemble them into one composed spec
305
+ // (deterministic), then run the document-level cross-area review/edit loop
306
+ // (CTSO-CONV-5) — duplication, terminology drift, and seam gaps that only a
307
+ // whole-document view catches — before the skill presents the result.
308
+ phase('Reconcile')
309
+
310
+ await agent(
311
+ [
312
+ `Run exactly this command: ${cli} cts compose-orchestrator`,
313
+ `It deterministically assembles the converged area partials into a single composed spec.`,
314
+ `End with a one-line confirmation of the composed-spec path it reports.`,
315
+ ].join('\n'),
316
+ { agentType: 'cts-worker', label: 'compose spec', phase: 'Reconcile' },
317
+ )
318
+
319
+ let crossAreaRounds = 0
320
+ let crossAreaConverged = false
321
+ for (let round = 1; round <= crossAreaRoundsCap; round++) {
322
+ crossAreaRounds = round
323
+ const verdict = await agent(
324
+ [
325
+ `You are a codebase-to-spec cross-area reviewer (document-level, independent).`,
326
+ ...follow(`${cli} cts dispatch-context cross-area-reviewer`),
327
+ `It tells you to review the composed spec for cross-area + document-level issues, record your verdict in the outline, and return it.`,
328
+ `Return { "result": "approved" | "needs-revision", "revisions": [...] }.`,
329
+ ].join('\n'),
330
+ {
331
+ schema: REVIEW_VERDICT_SCHEMA,
332
+ agentType: 'cts-worker',
333
+ label: `cross-area review r${round}`,
334
+ phase: 'Reconcile',
335
+ },
336
+ )
337
+
338
+ // Fail-closed: only an explicit "approved" verdict ends the loop. A null
339
+ // verdict (dead reviewer) is non-approval — fall through to edit/re-review.
340
+ if (verdict?.result === 'approved') {
341
+ crossAreaConverged = true
342
+ break
343
+ }
344
+ if (round === crossAreaRoundsCap) break
345
+
346
+ // The reviewer recorded its revisions in the outline's crossAreaReview thread;
347
+ // the compose editor self-composes from there and edits the composed spec.
348
+ await agent(
349
+ [
350
+ `You are a codebase-to-spec compose editor applying cross-area revisions to the composed spec.`,
351
+ ...follow(`${cli} cts dispatch-context compose-editor`),
352
+ `It embeds the cross-area reviewer's revisions and tells you to apply them, then validate.`,
353
+ `End with a one-line confirmation.`,
354
+ ].join('\n'),
355
+ { agentType: 'cts-worker', label: `compose edit r${round}`, phase: 'Reconcile' },
356
+ )
357
+ }
358
+
359
+ if (!crossAreaConverged) {
360
+ log(`cross-area review did not converge within ${crossAreaRoundsCap} rounds`)
361
+ }
362
+
363
+ return {
364
+ status: 'done',
365
+ areas: results,
366
+ converged_count: results.filter((r) => r.status === 'converged').length,
367
+ unconverged: results
368
+ .filter((r) => r.status === 'unconverged')
369
+ .map((r) => r.area_prefix),
370
+ failed: results.filter((r) => r.status === 'failed').map((r) => r.area_prefix),
371
+ total: areas.length,
372
+ cross_area_rounds: crossAreaRounds,
373
+ cross_area_converged: crossAreaConverged,
374
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Read this package's own package.json (name + version). Works from both
3
+ * src/ (vitest) and dist/ (compiled) since the two trees sit at the same
4
+ * depth under packages/cli.
5
+ */
6
+ export declare function getOwnPackage(): {
7
+ name: string;
8
+ version: string;
9
+ };
10
+ //# sourceMappingURL=own-package.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * Read this package's own package.json (name + version). Works from both
6
+ * src/ (vitest) and dist/ (compiled) since the two trees sit at the same
7
+ * depth under packages/cli.
8
+ */
9
+ export function getOwnPackage() {
10
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
11
+ return JSON.parse(readFileSync(pkgPath, "utf-8"));
12
+ }
13
+ //# sourceMappingURL=own-package.js.map
@@ -7,6 +7,11 @@ export interface SelectedProject {
7
7
  export interface SelectedTeam {
8
8
  teamId: string;
9
9
  tier: string;
10
+ /** Display name; present when selected via the shared selectTeam */
11
+ name?: string;
12
+ tierName?: string;
13
+ projectCount?: number;
14
+ projectLimit?: number;
10
15
  }
11
16
  /**
12
17
  * Prompt user to select a team from their available teams.
@@ -31,6 +31,10 @@ export async function selectTeam(client) {
31
31
  return {
32
32
  teamId: selectedTeamId,
33
33
  tier: selectedTeam?.tier ?? "free",
34
+ name: selectedTeam?.name,
35
+ tierName: selectedTeam?.tierName,
36
+ projectCount: selectedTeam?.projectCount,
37
+ projectLimit: selectedTeam?.projectLimit,
34
38
  };
35
39
  }
36
40
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.24.3",
3
+ "version": "0.26.0",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,7 @@
44
44
  "email": "support@dotrequirements.io"
45
45
  },
46
46
  "engines": {
47
- "node": ">=18"
47
+ "node": ">=20"
48
48
  },
49
49
  "dependencies": {
50
50
  "@babel/parser": "^7.28.5",
@@ -71,7 +71,7 @@
71
71
  "@types/node": "^20",
72
72
  "@types/uuid": "^11.0.0",
73
73
  "typescript": "^5",
74
- "vitest": "^4.0.0"
74
+ "vitest": "^4.1.8"
75
75
  },
76
76
  "files": [
77
77
  "dist",
@@ -1,76 +0,0 @@
1
- #!/bin/bash
2
- # PreToolUse hook for Task(subagent_type=cts-worker) dispatches.
3
- # Composes the worker's first-turn prompt by calling
4
- # `dotrequirements cts dispatch-context <dispatch-id>` and emits the
5
- # result via `updatedInput.prompt`.
6
- #
7
- # The orchestrator's convention: the original Task prompt MUST include
8
- # the verbatim token `dispatch-id=<id>` so the hook can extract it.
9
- #
10
- # Bundled with the codebase-to-spec skill (CTSO-CLI-1).
11
- #
12
- # CLI resolution order:
13
- # 1. DOTREQUIREMENTS_CLI env var (explicit override)
14
- # 2. If running inside the dotrequirements monorepo itself
15
- # ($CLAUDE_PROJECT_DIR/packages/cli/dist/cli.js exists), use that
16
- # local build. Lets dotrequirements developers use the orchestrator
17
- # against any worktree without a global install.
18
- # 3. `dotrequirements` from PATH (the canonical end-user setup).
19
-
20
- INPUT=$(cat)
21
- SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // ""')
22
-
23
- # Self-filter: only act on cts-worker dispatches.
24
- if [[ "$SUBAGENT_TYPE" != "cts-worker" ]]; then
25
- exit 0
26
- fi
27
-
28
- ORIGINAL_PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // ""')
29
-
30
- # Extract dispatch-id token from the original prompt.
31
- DISPATCH_ID=$(echo "$ORIGINAL_PROMPT" | grep -oE 'dispatch-id=[a-zA-Z0-9_-]+' | head -1 | sed 's/dispatch-id=//')
32
-
33
- if [[ -z "$DISPATCH_ID" ]]; then
34
- # No dispatch-id in the prompt — let it through unmodified.
35
- exit 0
36
- fi
37
-
38
- # Resolve the CLI invocation (see header for order).
39
- if [[ -n "${DOTREQUIREMENTS_CLI:-}" ]]; then
40
- CLI="$DOTREQUIREMENTS_CLI"
41
- elif [[ -n "${CLAUDE_PROJECT_DIR:-}" && -f "$CLAUDE_PROJECT_DIR/packages/cli/dist/cli.js" ]]; then
42
- CLI="node $CLAUDE_PROJECT_DIR/packages/cli/dist/cli.js"
43
- else
44
- CLI="dotrequirements"
45
- fi
46
-
47
- # Compose the dispatch context. CLI returns JSON like {"prompt": "..."}.
48
- COMPOSED=$($CLI cts dispatch-context "$DISPATCH_ID" 2>/dev/null)
49
- COMPOSE_STATUS=$?
50
-
51
- if [[ $COMPOSE_STATUS -ne 0 || -z "$COMPOSED" ]]; then
52
- # CLI call failed — let the dispatch through unmodified so the
53
- # worker's failure is legible.
54
- exit 0
55
- fi
56
-
57
- PROMPT=$(echo "$COMPOSED" | jq -r '.prompt // ""')
58
-
59
- if [[ -z "$PROMPT" ]]; then
60
- exit 0
61
- fi
62
-
63
- # Emit updatedInput so the worker subagent sees the composed prompt
64
- # as its first-turn input. (Field name is `updatedInput`, not
65
- # `modifiedInput` — verified by spike work; see working doc.)
66
- jq -nc --arg p "$PROMPT" '{
67
- hookSpecificOutput: {
68
- hookEventName: "PreToolUse",
69
- permissionDecision: "allow",
70
- updatedInput: {
71
- prompt: $p
72
- }
73
- }
74
- }'
75
-
76
- exit 0