@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,372 @@
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 PATH binary for installed users; the dev repo passes a
31
+ // `node <repo>/dist/cli.js` invocation explicitly.
32
+ const {
33
+ cli = 'dotrequirements',
34
+ roundsCap = 5,
35
+ outlineRoundsCap = 4,
36
+ crossAreaRoundsCap = 5,
37
+ } = input
38
+
39
+ // --- Schemas (mirrors of the JSON Schemas in schemas.ts; a sync test guards drift) ---
40
+
41
+ const PARTIAL_RESULT_SCHEMA = {
42
+ type: 'object',
43
+ properties: {
44
+ area_prefix: { type: 'string' },
45
+ partial_path: { type: 'string' },
46
+ status: { type: 'string', enum: ['drafted', 'skipped-resume'] },
47
+ requirement_count: { type: 'integer', minimum: 0 },
48
+ },
49
+ required: ['area_prefix', 'partial_path', 'status'],
50
+ additionalProperties: false,
51
+ }
52
+
53
+ const REVIEW_VERDICT_SCHEMA = {
54
+ type: 'object',
55
+ properties: {
56
+ result: { type: 'string', enum: ['approved', 'needs-revision'] },
57
+ revisions: { type: 'array', items: { type: 'string' } },
58
+ },
59
+ required: ['result'],
60
+ additionalProperties: false,
61
+ }
62
+
63
+ // Shape `cts dispatch-spec` emits (one entry per approved-outline area).
64
+ const AREAS_SCHEMA = {
65
+ type: 'object',
66
+ properties: {
67
+ areas: {
68
+ type: 'array',
69
+ items: {
70
+ type: 'object',
71
+ properties: {
72
+ dispatch_id: { type: 'string' },
73
+ area_prefix: { type: 'string' },
74
+ area_name: { type: 'string' },
75
+ output_path: { type: 'string' },
76
+ },
77
+ required: ['dispatch_id', 'area_prefix', 'area_name', 'output_path'],
78
+ additionalProperties: false,
79
+ },
80
+ },
81
+ },
82
+ required: ['areas'],
83
+ additionalProperties: false,
84
+ }
85
+
86
+ // Thin self-compose wrapper: a worker fetches its full instructions from the CLI.
87
+ function follow(line) {
88
+ return [
89
+ `Run exactly this command: ${line}`,
90
+ `It prints JSON of the form {"prompt": "..."}. Read the "prompt" field and follow it EXACTLY.`,
91
+ ]
92
+ }
93
+
94
+ // ============================ Stage 0: outline ============================
95
+ // plan → review → revise → re-review, until the outline reviewer approves the
96
+ // area decomposition or the cap is hit. Sequential (one outline), so the
97
+ // reviewer's writes to the document-level review.thread never contend.
98
+ phase('Outline')
99
+
100
+ await agent(
101
+ [
102
+ `You are a codebase-to-spec planner. Produce the behavioral-area outline for the packed codebase.`,
103
+ ...follow(`${cli} cts dispatch-context planner-initial`),
104
+ `End with a one-line confirmation of the outline path you wrote.`,
105
+ ].join('\n'),
106
+ { agentType: 'cts-worker', label: 'plan outline', phase: 'Outline' },
107
+ )
108
+
109
+ let outlineApproved = false
110
+ for (let round = 1; round <= outlineRoundsCap; round++) {
111
+ const verdict = await agent(
112
+ [
113
+ `You are a codebase-to-spec outline reviewer (independent of the planner).`,
114
+ ...follow(`${cli} cts dispatch-context outline-reviewer`),
115
+ `It tells you to record your verdict in the outline's top-level review thread and return it.`,
116
+ `Return { "result": "approved" | "needs-revision", "revisions": [...] }.`,
117
+ ].join('\n'),
118
+ {
119
+ schema: REVIEW_VERDICT_SCHEMA,
120
+ agentType: 'cts-worker',
121
+ label: `review outline r${round}`,
122
+ phase: 'Outline',
123
+ },
124
+ )
125
+
126
+ // Fail-closed: only an explicit "approved" verdict advances. A null verdict
127
+ // (reviewer subagent died after retries) is treated as non-approval — fall
128
+ // through to revise/re-review so we never fan out an unreviewed outline.
129
+ if (verdict?.result === 'approved') {
130
+ outlineApproved = true
131
+ break
132
+ }
133
+ if (round === outlineRoundsCap) break
134
+
135
+ await agent(
136
+ [
137
+ `You are a codebase-to-spec planner applying the reviewer's outline revisions.`,
138
+ ...follow(`${cli} cts dispatch-context planner-revise-${round + 1}`),
139
+ `End with a one-line confirmation.`,
140
+ ].join('\n'),
141
+ { agentType: 'cts-worker', label: `revise outline r${round}`, phase: 'Outline' },
142
+ )
143
+ }
144
+
145
+ if (!outlineApproved) {
146
+ // No mid-run HITL: don't fan out a decomposition the reviewer wouldn't pass.
147
+ log(`outline did not converge within ${outlineRoundsCap} rounds`)
148
+ return {
149
+ status: 'outline-unconverged',
150
+ outline_rounds: outlineRoundsCap,
151
+ areas: [],
152
+ converged_count: 0,
153
+ unconverged: [],
154
+ failed: [],
155
+ total: 0,
156
+ }
157
+ }
158
+
159
+ // ===================== Stage 1+2: fan out + converge =====================
160
+ // Enumerate the approved outline's areas (an agent runs the CLI; the script
161
+ // can't), then run each area through specify → review/edit independently.
162
+ phase('Specify')
163
+
164
+ const enumerated = await agent(
165
+ [
166
+ `Run exactly this command: ${cli} cts dispatch-spec`,
167
+ `It prints a JSON array, one entry per area of the approved outline.`,
168
+ `Return { "areas": <that array, verbatim> }.`,
169
+ ].join('\n'),
170
+ { schema: AREAS_SCHEMA, agentType: 'cts-worker', label: 'enumerate areas', phase: 'Specify' },
171
+ )
172
+
173
+ // Fail-closed: a dead enumerate agent (null return) must surface as a failure,
174
+ // not read as an empty-but-successful decomposition that sails through fan-out
175
+ // and reconcile to a hollow `done`.
176
+ if (!enumerated) {
177
+ log('area enumeration failed — cannot fan out')
178
+ return {
179
+ status: 'enumerate-failed',
180
+ areas: [],
181
+ converged_count: 0,
182
+ unconverged: [],
183
+ failed: [],
184
+ total: 0,
185
+ }
186
+ }
187
+ const areas = enumerated.areas
188
+
189
+ async function specify(area) {
190
+ return agent(
191
+ [
192
+ `You are a codebase-to-spec specifier worker for the area "${area.area_name}" (prefix ${area.area_prefix}).`,
193
+ ``,
194
+ `Step 1 — Resume check. If a non-empty partial already exists at:`,
195
+ ` ${area.output_path}`,
196
+ ` then do NOT regenerate it. Return`,
197
+ ` { "area_prefix": "${area.area_prefix}", "partial_path": "${area.output_path}", "status": "skipped-resume" }.`,
198
+ ``,
199
+ `Step 2 — Otherwise, run exactly this command:`,
200
+ ` ${cli} cts dispatch-context specifier-${area.area_prefix}`,
201
+ ` Read the "prompt" field of its JSON output and follow it EXACTLY — which source files to`,
202
+ ` read, how to ground requirements in this area's customers, where to write the partial,`,
203
+ ` and how to validate + style-check it.`,
204
+ ``,
205
+ `Step 3 — Return`,
206
+ ` { "area_prefix": "${area.area_prefix}", "partial_path": "${area.output_path}",`,
207
+ ` "status": "drafted", "requirement_count": <number of requirements you wrote> }.`,
208
+ ].join('\n'),
209
+ {
210
+ schema: PARTIAL_RESULT_SCHEMA,
211
+ agentType: 'cts-worker',
212
+ label: `specify ${area.area_name}`,
213
+ phase: 'Specify',
214
+ },
215
+ )
216
+ }
217
+
218
+ async function reviewEditLoop(specifyResult, area) {
219
+ // Fail-closed (CTSO-INTEG-3.2): a dead specifier (null return) means there may
220
+ // be no partial to review — record the failure and continue with the other
221
+ // areas, instead of burning review/edit rounds on a draft that may not exist.
222
+ if (!specifyResult) {
223
+ log(`area ${area.area_prefix}: specifier failed — recording and moving on`)
224
+ return {
225
+ area_prefix: area.area_prefix,
226
+ area_name: area.area_name,
227
+ partial_path: area.output_path,
228
+ status: 'failed',
229
+ rounds: 0,
230
+ }
231
+ }
232
+
233
+ for (let round = 1; round <= roundsCap; round++) {
234
+ const verdict = await agent(
235
+ [
236
+ `You are a codebase-to-spec reviewer for the area "${area.area_name}" (prefix ${area.area_prefix}).`,
237
+ ...follow(`${cli} cts dispatch-context reviewer-${area.area_prefix}`),
238
+ `It tells you to review the partial, record your verdict in the outline, and return it.`,
239
+ `Return { "result": "approved" | "needs-revision", "revisions": [...] }.`,
240
+ ].join('\n'),
241
+ {
242
+ schema: REVIEW_VERDICT_SCHEMA,
243
+ agentType: 'cts-worker',
244
+ label: `review ${area.area_name} r${round}`,
245
+ phase: 'Converge',
246
+ },
247
+ )
248
+
249
+ if (verdict?.result === 'approved') {
250
+ return {
251
+ area_prefix: area.area_prefix,
252
+ area_name: area.area_name,
253
+ partial_path: area.output_path,
254
+ status: 'converged',
255
+ rounds: round - 1,
256
+ }
257
+ }
258
+
259
+ // Fail-closed: a null verdict (dead reviewer) is NOT approval — but it also
260
+ // recorded no fresh revisions in the outline, so dispatching an editor here
261
+ // would only re-apply the previous round's already-applied list (or have
262
+ // nothing to act on in round 1). Skip straight to the next review round.
263
+ if (!verdict) continue
264
+
265
+ // Mirror the outline and cross-area loops: the final round's verdict is
266
+ // final. Without this guard a last editor pass would run whose output is
267
+ // never re-reviewed before composition.
268
+ if (round === roundsCap) break
269
+
270
+ await agent(
271
+ [
272
+ `You are a codebase-to-spec editor for the area "${area.area_name}" (prefix ${area.area_prefix}).`,
273
+ ...follow(`${cli} cts dispatch-context editor-${area.area_prefix}`),
274
+ `It embeds the reviewer's revisions and tells you to apply them, then validate + style-check.`,
275
+ `Return { "area_prefix": "${area.area_prefix}", "partial_path": "${area.output_path}",`,
276
+ ` "status": "drafted", "requirement_count": <number of requirements now in the partial> }.`,
277
+ ].join('\n'),
278
+ {
279
+ schema: PARTIAL_RESULT_SCHEMA,
280
+ agentType: 'cts-worker',
281
+ label: `edit ${area.area_name} r${round}`,
282
+ phase: 'Converge',
283
+ },
284
+ )
285
+ }
286
+
287
+ log(`area ${area.area_prefix} hit roundsCap=${roundsCap} without approval`)
288
+ return {
289
+ area_prefix: area.area_prefix,
290
+ area_name: area.area_name,
291
+ partial_path: area.output_path,
292
+ status: 'unconverged',
293
+ rounds: roundsCap,
294
+ }
295
+ }
296
+
297
+ log(`Outline approved — fanning out ${areas.length} area(s)`)
298
+
299
+ const results = (await pipeline(areas, specify, reviewEditLoop)).filter(Boolean)
300
+
301
+ // ===================== Stage 3: compose + cross-area reconcile =====================
302
+ // The per-area partials have converged. Assemble them into one composed spec
303
+ // (deterministic), then run the document-level cross-area review/edit loop
304
+ // (CTSO-CONV-5) — duplication, terminology drift, and seam gaps that only a
305
+ // whole-document view catches — before the skill presents the result.
306
+ phase('Reconcile')
307
+
308
+ await agent(
309
+ [
310
+ `Run exactly this command: ${cli} cts compose-orchestrator`,
311
+ `It deterministically assembles the converged area partials into a single composed spec.`,
312
+ `End with a one-line confirmation of the composed-spec path it reports.`,
313
+ ].join('\n'),
314
+ { agentType: 'cts-worker', label: 'compose spec', phase: 'Reconcile' },
315
+ )
316
+
317
+ let crossAreaRounds = 0
318
+ let crossAreaConverged = false
319
+ for (let round = 1; round <= crossAreaRoundsCap; round++) {
320
+ crossAreaRounds = round
321
+ const verdict = await agent(
322
+ [
323
+ `You are a codebase-to-spec cross-area reviewer (document-level, independent).`,
324
+ ...follow(`${cli} cts dispatch-context cross-area-reviewer`),
325
+ `It tells you to review the composed spec for cross-area + document-level issues, record your verdict in the outline, and return it.`,
326
+ `Return { "result": "approved" | "needs-revision", "revisions": [...] }.`,
327
+ ].join('\n'),
328
+ {
329
+ schema: REVIEW_VERDICT_SCHEMA,
330
+ agentType: 'cts-worker',
331
+ label: `cross-area review r${round}`,
332
+ phase: 'Reconcile',
333
+ },
334
+ )
335
+
336
+ // Fail-closed: only an explicit "approved" verdict ends the loop. A null
337
+ // verdict (dead reviewer) is non-approval — fall through to edit/re-review.
338
+ if (verdict?.result === 'approved') {
339
+ crossAreaConverged = true
340
+ break
341
+ }
342
+ if (round === crossAreaRoundsCap) break
343
+
344
+ // The reviewer recorded its revisions in the outline's crossAreaReview thread;
345
+ // the compose editor self-composes from there and edits the composed spec.
346
+ await agent(
347
+ [
348
+ `You are a codebase-to-spec compose editor applying cross-area revisions to the composed spec.`,
349
+ ...follow(`${cli} cts dispatch-context compose-editor`),
350
+ `It embeds the cross-area reviewer's revisions and tells you to apply them, then validate.`,
351
+ `End with a one-line confirmation.`,
352
+ ].join('\n'),
353
+ { agentType: 'cts-worker', label: `compose edit r${round}`, phase: 'Reconcile' },
354
+ )
355
+ }
356
+
357
+ if (!crossAreaConverged) {
358
+ log(`cross-area review did not converge within ${crossAreaRoundsCap} rounds`)
359
+ }
360
+
361
+ return {
362
+ status: 'done',
363
+ areas: results,
364
+ converged_count: results.filter((r) => r.status === 'converged').length,
365
+ unconverged: results
366
+ .filter((r) => r.status === 'unconverged')
367
+ .map((r) => r.area_prefix),
368
+ failed: results.filter((r) => r.status === 'failed').map((r) => r.area_prefix),
369
+ total: areas.length,
370
+ cross_area_rounds: crossAreaRounds,
371
+ cross_area_converged: crossAreaConverged,
372
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.24.2",
3
+ "version": "0.25.0",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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",