@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
@@ -189,6 +189,250 @@ export const SPEC_REVIEW_JSON_SCHEMA = {
189
189
  ],
190
190
  additionalProperties: false,
191
191
  };
192
+ // ---------- Conversational orchestrator outline (Phase 2b refactor) ----------
193
+ //
194
+ // The conversational orchestrator uses a single evolving `outline.yaml` as
195
+ // the pipeline's substrate. The outline carries both the behavioral spec
196
+ // content (areas, customers, source_files) AND its own lifecycle state
197
+ // (review.result + review.thread). This is distinct from the legacy
198
+ // `OutlineSchema` above, which the `cts run` pipeline still uses.
199
+ //
200
+ // Field naming uses "result" rather than "verdict" deliberately: review is
201
+ // a collaborative interaction with the worker, not a juridical ruling.
202
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
203
+ export const ConversationalCustomerSchema = z.object({
204
+ description: z.string().min(1),
205
+ });
206
+ /**
207
+ * One entry in the review thread (project-level or per-area). Discriminated
208
+ * on `result`:
209
+ *
210
+ * - `approved`: no revisions required.
211
+ * - `needs-revision`: must include a non-empty `revisions` list — each
212
+ * entry is a clear, actionable instruction for the next planner pass.
213
+ */
214
+ export const ConversationalReviewEntrySchema = z.discriminatedUnion("result", [
215
+ z.object({
216
+ result: z.literal("approved"),
217
+ }),
218
+ z.object({
219
+ result: z.literal("needs-revision"),
220
+ revisions: z.array(z.string()).min(1),
221
+ }),
222
+ ]);
223
+ /**
224
+ * Review state. Used both at the project level (outline.review) and per
225
+ * area (area.review). `result` mirrors the latest thread entry's result
226
+ * so callers can query state without walking the thread.
227
+ */
228
+ export const ConversationalReviewSchema = z.object({
229
+ result: z.enum(["approved", "needs-revision"]),
230
+ thread: z.array(ConversationalReviewEntrySchema).min(1),
231
+ });
232
+ export const ConversationalAreaSchema = z.object({
233
+ name: z.string().min(1),
234
+ prefix: z
235
+ .string()
236
+ .regex(/^[A-Z][A-Z0-9_]*$/, "prefix must be uppercase alphanumeric/underscore"),
237
+ description: z.string().min(1),
238
+ source_files: z.array(z.string()).min(0),
239
+ customers: z.array(ConversationalCustomerSchema).min(1),
240
+ /**
241
+ * Per-area review state — same shape as the project-level review.
242
+ * Optional: planner output doesn't include it; CA adds it after reviewing
243
+ * a partial. Tracks the per-area iteration loop (specify → review →
244
+ * editor → re-review → approved) the same way `outline.review` tracks
245
+ * the outline iteration loop.
246
+ */
247
+ review: ConversationalReviewSchema.optional(),
248
+ });
249
+ export const ConversationalOutlineSchema = z.object({
250
+ title: z.string().min(1),
251
+ defaultPrefix: z
252
+ .string()
253
+ .regex(/^[A-Z][A-Z0-9_]*$/, "defaultPrefix must be uppercase alphanumeric/underscore"),
254
+ summary: z.string().min(1),
255
+ /**
256
+ * Outline-approval review (the outline iteration loop). `compose-orchestrator`
257
+ * gates composition on `review.result === "approved"`, so this field is
258
+ * distinct from and must never be conflated with `crossAreaReview` below.
259
+ */
260
+ review: ConversationalReviewSchema.optional(),
261
+ /**
262
+ * Document-level cross-area review state (CTSO-CONV-5). Recorded after the
263
+ * per-area partials are composed: the cross-area reviewer appends verdicts
264
+ * here and the compose-editor reads the latest entry's revisions — the same
265
+ * auditable thread pattern as `area.review`, kept at the top level because the
266
+ * review spans the whole composed spec rather than one area. Separate from
267
+ * `review` so cross-area iteration never disturbs outline approval.
268
+ */
269
+ crossAreaReview: ConversationalReviewSchema.optional(),
270
+ areas: z.array(ConversationalAreaSchema).min(1),
271
+ });
272
+ /**
273
+ * Parse YAML text against the conversational outline schema. Throws with
274
+ * a descriptive message on parse or validation failure.
275
+ */
276
+ export function parseConversationalOutline(text) {
277
+ let raw;
278
+ try {
279
+ raw = parseYaml(text);
280
+ }
281
+ catch (err) {
282
+ throw new Error(`Outline YAML is invalid: ${err instanceof Error ? err.message : String(err)}`);
283
+ }
284
+ return ConversationalOutlineSchema.parse(raw);
285
+ }
286
+ /**
287
+ * Synthesize a short customer `name` from a conversational customer's
288
+ * `description`. The orchestrator's `ConversationalCustomerSchema` only
289
+ * carries description, but the legacy `CustomerSchema` requires both
290
+ * `name` and `description`. Adapter use only — downstream compose/present
291
+ * stages don't read `name`, so this is a schema-shape bridge, not a
292
+ * semantic field.
293
+ *
294
+ * Heuristic: descriptions like "Priya, a CTS pipeline operator" yield
295
+ * "Priya" via first comma-segment. Otherwise truncate to 40 chars.
296
+ */
297
+ function synthesizeCustomerName(description) {
298
+ const firstSegment = description.split(",")[0]?.trim() ?? "";
299
+ if (firstSegment.length > 0 && firstSegment.length <= 40) {
300
+ return firstSegment;
301
+ }
302
+ return description.slice(0, 40).trim() || description;
303
+ }
304
+ /**
305
+ * Adapt a conversational orchestrator outline to the legacy Outline shape
306
+ * for use with stages that still take the legacy schema (compose, present).
307
+ *
308
+ * Discards orchestrator-only fields (review); maps `source_files` → `files`;
309
+ * bridges the customer-shape difference by synthesizing a `name` from each
310
+ * conversational customer's description.
311
+ */
312
+ export function conversationalOutlineToLegacy(outline) {
313
+ return {
314
+ title: outline.title,
315
+ defaultPrefix: outline.defaultPrefix,
316
+ summary: outline.summary,
317
+ areas: outline.areas.map((a) => ({
318
+ name: a.name,
319
+ description: a.description,
320
+ prefix: a.prefix,
321
+ files: a.source_files,
322
+ customers: a.customers.map((c) => ({
323
+ name: synthesizeCustomerName(c.description),
324
+ description: c.description,
325
+ })),
326
+ })),
327
+ };
328
+ }
329
+ /**
330
+ * Serialize a conversational outline back to YAML text. Uses literal-block
331
+ * multi-line strings (`|`) where possible for readability.
332
+ */
333
+ export function stringifyConversationalOutline(outline) {
334
+ return stringifyYaml(outline, {
335
+ lineWidth: 0,
336
+ blockQuote: "literal",
337
+ });
338
+ }
339
+ // ---------- Workflow orchestrator worker I/O ----------
340
+ //
341
+ // The workflow orchestrator (the `specify-codebase` dynamic workflow) fans out
342
+ // specifier / editor / reviewer subagents and receives their results back as
343
+ // schema-validated values via the workflow runtime's `schema` option. The JSON
344
+ // Schemas below are the contract the runtime constrains each agent to; the Zod
345
+ // schemas validate the same shapes on the CLI/test side. Because the workflow
346
+ // script can't import this module, the JSON Schemas are mirrored inline in the
347
+ // workflow file; a template-sync test guards against drift.
348
+ /**
349
+ * Result a specifier or editor worker returns to the workflow after writing
350
+ * (or skipping) an area's partial.
351
+ *
352
+ * - `drafted`: the worker composed and wrote the partial this run.
353
+ * - `skipped-resume`: a non-empty partial already existed, so the worker left
354
+ * it in place (idempotent re-entry — see CTSO-INTEG-2).
355
+ */
356
+ export const PartialResultStatusValues = ["drafted", "skipped-resume"];
357
+ export const PartialResultSchema = z.object({
358
+ area_prefix: z.string().min(1),
359
+ partial_path: z.string().min(1),
360
+ status: z.enum(PartialResultStatusValues),
361
+ requirement_count: z.number().int().nonnegative().optional(),
362
+ });
363
+ export const PARTIAL_RESULT_JSON_SCHEMA = {
364
+ type: "object",
365
+ properties: {
366
+ area_prefix: { type: "string" },
367
+ partial_path: { type: "string" },
368
+ status: {
369
+ type: "string",
370
+ enum: PartialResultStatusValues,
371
+ },
372
+ requirement_count: { type: "integer", minimum: 0 },
373
+ },
374
+ required: ["area_prefix", "partial_path", "status"],
375
+ additionalProperties: false,
376
+ };
377
+ /**
378
+ * Verdict a reviewer worker returns to the workflow. Same shape as one
379
+ * `ConversationalReviewEntry` (discriminated on `result`): `approved` carries
380
+ * no revisions; `needs-revision` must carry a non-empty `revisions` list. This
381
+ * verdict drives the per-area convergence loop (CTSO-CONV-4).
382
+ *
383
+ * The JSON Schema form is flat (result + optional revisions) — JSON Schema
384
+ * can't express the discriminated-union constraint as cleanly as Zod. The
385
+ * runtime retries on mismatch, the prompt instructs the reviewer to include
386
+ * revisions exactly when the result is needs-revision, and `parseReviewVerdict`
387
+ * enforces the stricter Zod constraint on the CLI side.
388
+ */
389
+ export const REVIEW_VERDICT_JSON_SCHEMA = {
390
+ type: "object",
391
+ properties: {
392
+ result: { type: "string", enum: ["approved", "needs-revision"] },
393
+ revisions: { type: "array", items: { type: "string" } },
394
+ },
395
+ required: ["result"],
396
+ additionalProperties: false,
397
+ };
398
+ /**
399
+ * Parse + validate a specifier/editor worker's JSON result.
400
+ */
401
+ export function parsePartialResult(text) {
402
+ const trimmed = text.trim();
403
+ let raw;
404
+ try {
405
+ raw = JSON.parse(trimmed);
406
+ }
407
+ catch {
408
+ const match = trimmed.match(/(\{[\s\S]*\})/);
409
+ if (!match) {
410
+ throw new Error("Partial result is not valid JSON and contains no JSON object");
411
+ }
412
+ raw = JSON.parse(match[1]);
413
+ }
414
+ return PartialResultSchema.parse(raw);
415
+ }
416
+ /**
417
+ * Parse + validate a reviewer worker's JSON verdict. Reuses
418
+ * `ConversationalReviewEntrySchema`, so a `needs-revision` verdict requires a
419
+ * non-empty `revisions` list.
420
+ */
421
+ export function parseReviewVerdict(text) {
422
+ const trimmed = text.trim();
423
+ let raw;
424
+ try {
425
+ raw = JSON.parse(trimmed);
426
+ }
427
+ catch {
428
+ const match = trimmed.match(/(\{[\s\S]*\})/);
429
+ if (!match) {
430
+ throw new Error("Review verdict is not valid JSON and contains no JSON object");
431
+ }
432
+ raw = JSON.parse(match[1]);
433
+ }
434
+ return ConversationalReviewEntrySchema.parse(raw);
435
+ }
192
436
  export function parseSpecReview(text) {
193
437
  const trimmed = text.trim();
194
438
  let raw;
@@ -1,34 +1,42 @@
1
1
  /**
2
2
  * Skill installation logic for the codebase-to-spec skill.
3
3
  *
4
- * The skill is a thin conversational wrapper around `dotrequirements cts run`.
5
- * It ships as a SKILL.md template bundled in the CLI package, and this module
6
- * copies it into the host's skill directory (default: `.claude/skills/` for
7
- * Claude Code).
4
+ * Ships as a bundle: SKILL.md + cts-worker agent definition + the `codebase-to-spec`
5
+ * dynamic-workflow script. The skill body (the conversational orchestrator)
6
+ * launches the workflow, which dispatches cts-worker agents; both companions
7
+ * must be present for the skill to run. For `project` and `global` scope, all
8
+ * three pieces are installed in one shot (under the project's `.claude/` or the
9
+ * user's `~/.claude/`). Only a `custom` target dir is skill-only, because
10
+ * assistants do not auto-discover agents or workflows from arbitrary paths.
8
11
  *
9
12
  * Host portability (CTS-SKILL-5): the install logic supports any host that
10
13
  * follows the Agent Skills format. The default target is Claude Code's
11
14
  * convention; `--target-dir` lets users place the skill anywhere.
12
15
  *
13
16
  * Requirements covered:
14
- * - CTS-SKILL-1: skill artifact exists and points users at `dotrequirements cts run`
15
- * - CTS-SKILL-5: install logic is host-agnostic and works on any host with
16
- * Agent Skills format support
17
+ * - CTS-SKILL-1, CTS-SKILL-5
18
+ * - CTSO-INSTALL-1 (bundles the skill, the cts-worker agent, and the workflow)
17
19
  */
18
20
  export type SkillInstallScope = "project" | "global" | "custom";
19
21
  export interface SkillInstallOptions {
20
22
  /**
21
- * Where to install:
22
- * - `project` (default): `<projectRoot>/.claude/skills/codebase-to-spec/`
23
- * - `global`: `~/.claude/skills/codebase-to-spec/`
24
- * - `custom`: requires `targetDir`
23
+ * Where to install. `project` and `global` install the full bundle (skill +
24
+ * cts-worker agent + specify-codebase workflow) under the corresponding
25
+ * `.claude/` base, so the skill can launch the workflow; `custom` installs
26
+ * the skill body only.
27
+ * - `project` (default): `<projectRoot>/.claude/{skills,agents,workflows}/`
28
+ * - `global`: `<globalRoot>/.claude/{skills,agents,workflows}/`
29
+ * - `custom`: `<targetDir>/codebase-to-spec/SKILL.md` only — Claude Code does
30
+ * not auto-discover agents or workflows from an arbitrary directory
25
31
  */
26
32
  scope?: SkillInstallScope;
27
33
  /** Required when `scope === 'custom'`. The skill directory will be created here. */
28
34
  targetDir?: string;
29
35
  /** Project root (used when scope === 'project'). Defaults to cwd. */
30
36
  projectRoot?: string;
31
- /** Overwrite existing SKILL.md if present. Default false. */
37
+ /** Home root for `global` scope (the parent of `.claude`). Defaults to homedir(); tests inject a temp dir. */
38
+ globalRoot?: string;
39
+ /** Overwrite existing SKILL.md / agent / workflow if present. Default false. */
32
40
  overwrite?: boolean;
33
41
  }
34
42
  export interface SkillInstallResult {
@@ -38,6 +46,11 @@ export interface SkillInstallResult {
38
46
  installed: boolean;
39
47
  /** True if an existing file was replaced. */
40
48
  overwrote: boolean;
49
+ /** Companion files installed alongside the skill (project + global scope). */
50
+ companions?: {
51
+ agentPath: string;
52
+ workflowPath: string;
53
+ };
41
54
  }
42
55
  /**
43
56
  * Resolve the directory in which the skill should live, given the user's
@@ -50,8 +63,22 @@ export declare function resolveSkillDir(options: SkillInstallOptions): string;
50
63
  */
51
64
  export declare function loadSkillTemplate(): string;
52
65
  /**
53
- * Install (or refuse to overwrite) the codebase-to-spec SKILL.md at the
54
- * resolved location.
66
+ * Load the bundled cts-worker agent definition template.
67
+ */
68
+ export declare function loadAgentTemplate(): string;
69
+ /**
70
+ * Load the bundled specify-codebase dynamic-workflow script template.
71
+ */
72
+ export declare function loadWorkflowTemplate(): string;
73
+ /**
74
+ * Install (or refuse to overwrite) the codebase-to-spec skill bundle.
75
+ *
76
+ * For `project` and `global` scope: installs the full bundle — the skill body,
77
+ * the cts-worker agent, and the specify-codebase workflow — under the matching
78
+ * `.claude/` base (the project's, or the user's `~/.claude/`).
79
+ *
80
+ * For `custom` scope: installs only the skill body, because Claude Code does
81
+ * not auto-discover agents or workflows from an arbitrary directory.
55
82
  */
56
83
  export declare function installSkill(options?: SkillInstallOptions): SkillInstallResult;
57
84
  //# sourceMappingURL=skill-install.d.ts.map
@@ -1,19 +1,21 @@
1
1
  /**
2
2
  * Skill installation logic for the codebase-to-spec skill.
3
3
  *
4
- * The skill is a thin conversational wrapper around `dotrequirements cts run`.
5
- * It ships as a SKILL.md template bundled in the CLI package, and this module
6
- * copies it into the host's skill directory (default: `.claude/skills/` for
7
- * Claude Code).
4
+ * Ships as a bundle: SKILL.md + cts-worker agent definition + the `codebase-to-spec`
5
+ * dynamic-workflow script. The skill body (the conversational orchestrator)
6
+ * launches the workflow, which dispatches cts-worker agents; both companions
7
+ * must be present for the skill to run. For `project` and `global` scope, all
8
+ * three pieces are installed in one shot (under the project's `.claude/` or the
9
+ * user's `~/.claude/`). Only a `custom` target dir is skill-only, because
10
+ * assistants do not auto-discover agents or workflows from arbitrary paths.
8
11
  *
9
12
  * Host portability (CTS-SKILL-5): the install logic supports any host that
10
13
  * follows the Agent Skills format. The default target is Claude Code's
11
14
  * convention; `--target-dir` lets users place the skill anywhere.
12
15
  *
13
16
  * Requirements covered:
14
- * - CTS-SKILL-1: skill artifact exists and points users at `dotrequirements cts run`
15
- * - CTS-SKILL-5: install logic is host-agnostic and works on any host with
16
- * Agent Skills format support
17
+ * - CTS-SKILL-1, CTS-SKILL-5
18
+ * - CTSO-INSTALL-1 (bundles the skill, the cts-worker agent, and the workflow)
17
19
  */
18
20
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
21
  import { homedir } from "node:os";
@@ -32,7 +34,7 @@ export function resolveSkillDir(options) {
32
34
  return join(root, ".claude", "skills", "codebase-to-spec");
33
35
  }
34
36
  case "global":
35
- return join(homedir(), ".claude", "skills", "codebase-to-spec");
37
+ return join(options.globalRoot ?? homedir(), ".claude", "skills", "codebase-to-spec");
36
38
  case "custom":
37
39
  if (!options.targetDir) {
38
40
  throw new Error('targetDir is required when scope === "custom"');
@@ -49,31 +51,78 @@ export function loadSkillTemplate() {
49
51
  return loadTemplate("skills/codebase-to-spec/SKILL.md");
50
52
  }
51
53
  /**
52
- * Install (or refuse to overwrite) the codebase-to-spec SKILL.md at the
53
- * resolved location.
54
+ * Load the bundled cts-worker agent definition template.
55
+ */
56
+ export function loadAgentTemplate() {
57
+ return loadTemplate("agents/cts-worker.md");
58
+ }
59
+ /**
60
+ * Load the bundled specify-codebase dynamic-workflow script template.
61
+ */
62
+ export function loadWorkflowTemplate() {
63
+ return loadTemplate("workflows/specify-codebase.js");
64
+ }
65
+ /**
66
+ * Install (or refuse to overwrite) one template file at a target path.
67
+ * Returns true if a write happened.
68
+ */
69
+ function installTemplateFile(templateContent, targetPath, overwrite, description) {
70
+ const exists = existsSync(targetPath);
71
+ if (exists && !overwrite) {
72
+ const onDisk = readFileSync(targetPath, "utf-8");
73
+ if (onDisk === templateContent) {
74
+ return { installed: false, overwrote: false };
75
+ }
76
+ throw new Error(`${description} already exists at ${targetPath} and differs from the bundled template. Pass --overwrite to replace it, or delete the file first.`);
77
+ }
78
+ mkdirSync(dirname(targetPath), { recursive: true });
79
+ writeFileSync(targetPath, templateContent, "utf-8");
80
+ return { installed: true, overwrote: exists };
81
+ }
82
+ /**
83
+ * Install (or refuse to overwrite) the codebase-to-spec skill bundle.
84
+ *
85
+ * For `project` and `global` scope: installs the full bundle — the skill body,
86
+ * the cts-worker agent, and the specify-codebase workflow — under the matching
87
+ * `.claude/` base (the project's, or the user's `~/.claude/`).
88
+ *
89
+ * For `custom` scope: installs only the skill body, because Claude Code does
90
+ * not auto-discover agents or workflows from an arbitrary directory.
54
91
  */
55
92
  export function installSkill(options = {}) {
56
93
  const skillDir = resolveSkillDir(options);
57
94
  const skillPath = join(skillDir, "SKILL.md");
58
- const template = loadSkillTemplate();
59
- const exists = existsSync(skillPath);
60
- if (exists && !options.overwrite) {
61
- // Idempotent no-op: if the on-disk content already matches the template,
62
- // there's nothing to do and we shouldn't surface that as a conflict.
63
- const onDisk = readFileSync(skillPath, "utf-8");
64
- if (onDisk === template) {
65
- return { installedPath: skillPath, installed: false, overwrote: false };
66
- }
67
- // Stale or hand-edited copy: refuse without --overwrite so we don't
68
- // silently clobber user changes.
69
- throw new Error(`SKILL.md already exists at ${skillPath} and differs from the bundled template. Pass --overwrite to replace it, or delete the file first.`);
95
+ const skillTemplate = loadSkillTemplate();
96
+ const skillResult = installTemplateFile(skillTemplate, skillPath, options.overwrite === true, "SKILL.md");
97
+ // A custom target dir is an arbitrary location CC won't read agents or
98
+ // workflows from, so it gets the skill body only. Project and global both
99
+ // install the full bundle under their `.claude/` base.
100
+ const scope = options.scope ?? "project";
101
+ if (scope === "custom") {
102
+ return {
103
+ installedPath: skillPath,
104
+ installed: skillResult.installed,
105
+ overwrote: skillResult.overwrote,
106
+ };
70
107
  }
71
- mkdirSync(dirname(skillPath), { recursive: true });
72
- writeFileSync(skillPath, template, "utf-8");
108
+ const claudeBase = scope === "global"
109
+ ? join(options.globalRoot ?? homedir(), ".claude")
110
+ : join(options.projectRoot ?? process.cwd(), ".claude");
111
+ const agentPath = join(claudeBase, "agents", "cts-worker.md");
112
+ const workflowPath = join(claudeBase, "workflows", "specify-codebase.js");
113
+ // Install the cts-worker agent definition.
114
+ installTemplateFile(loadAgentTemplate(), agentPath, options.overwrite === true, "cts-worker agent definition");
115
+ // Install the specify-codebase workflow script (discovered by name from
116
+ // .claude/workflows/; no settings registration needed, unlike a hook).
117
+ installTemplateFile(loadWorkflowTemplate(), workflowPath, options.overwrite === true, "specify-codebase workflow script");
73
118
  return {
74
119
  installedPath: skillPath,
75
- installed: true,
76
- overwrote: exists,
120
+ installed: skillResult.installed,
121
+ overwrote: skillResult.overwrote,
122
+ companions: {
123
+ agentPath,
124
+ workflowPath,
125
+ },
77
126
  };
78
127
  }
79
128
  //# sourceMappingURL=skill-install.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec compose-orchestrator` subcommand.
3
+ *
4
+ * Conversational-orchestrator variant of `compose`. Reads outline.yaml
5
+ * (must be approved) + per-area partials, writes the composed spec to
6
+ * .dotrequirements-cache/spec-composed.md. Wraps the legacy `runCompose`
7
+ * by adapting outline.yaml to the legacy Outline shape.
8
+ *
9
+ * Requirements covered:
10
+ * - CTS-COMPOSE-1, CTS-COMPOSE-2 (reuses the legacy compose logic)
11
+ * - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
12
+ */
13
+ export declare function composeOrchestratorCommand(): Promise<void>;
14
+ //# sourceMappingURL=compose-orchestrator.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec compose-orchestrator` subcommand.
3
+ *
4
+ * Conversational-orchestrator variant of `compose`. Reads outline.yaml
5
+ * (must be approved) + per-area partials, writes the composed spec to
6
+ * .dotrequirements-cache/spec-composed.md. Wraps the legacy `runCompose`
7
+ * by adapting outline.yaml to the legacy Outline shape.
8
+ *
9
+ * Requirements covered:
10
+ * - CTS-COMPOSE-1, CTS-COMPOSE-2 (reuses the legacy compose logic)
11
+ * - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
12
+ */
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { cachePaths } from "../../codebase-to-spec/cache.js";
15
+ import { runCompose } from "../../codebase-to-spec/compose.js";
16
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
17
+ import { conversationalOutlineToLegacy, parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
18
+ import { findProjectRoot } from "../../utils/project-settings.js";
19
+ export async function composeOrchestratorCommand() {
20
+ const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
21
+ const paths = cachePaths(projectRoot);
22
+ if (!existsSync(paths.outline)) {
23
+ process.stderr.write(`No outline.yaml found at ${paths.outline}. Run the orchestrator's planner first.\n`);
24
+ process.exitCode = ExitCode.MissingInput;
25
+ return;
26
+ }
27
+ let outline;
28
+ try {
29
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
30
+ }
31
+ catch (err) {
32
+ process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
33
+ process.exitCode = ExitCode.MissingInput;
34
+ return;
35
+ }
36
+ if (!outline.review || outline.review.result !== "approved") {
37
+ process.stderr.write(`Outline at ${paths.outline} is not approved (review.result is "${outline.review?.result ?? "absent"}"). Approve the outline before composing.\n`);
38
+ process.exitCode = ExitCode.MissingInput;
39
+ return;
40
+ }
41
+ const legacyOutline = conversationalOutlineToLegacy(outline);
42
+ const result = runCompose({
43
+ outline: legacyOutline,
44
+ partialPathFor: (sanitized) => paths.partial(sanitized),
45
+ composedPath: paths.composedSpec,
46
+ });
47
+ process.stdout.write(`Composed spec → ${result.composedPath} (${result.partialsIncluded} partials included, ${result.partialsMissing} missing)\n`);
48
+ if (result.validationError) {
49
+ process.stderr.write(`Composed spec failed validation:\n${result.validationError}\n`);
50
+ process.exitCode = ExitCode.StageFailed;
51
+ return;
52
+ }
53
+ }
54
+ //# sourceMappingURL=compose-orchestrator.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-context <dispatch-id>` subcommand.
3
+ *
4
+ * Returns the composed prompt for a worker dispatch as a JSON object on stdout.
5
+ * Run by cts-worker subagents at the start of their turn to fetch the composed
6
+ * first-turn prompt for their dispatch-id (see the specify-codebase workflow).
7
+ */
8
+ export declare function dispatchContextCommand(dispatchId: string): Promise<void>;
9
+ //# sourceMappingURL=dispatch-context.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-context <dispatch-id>` subcommand.
3
+ *
4
+ * Returns the composed prompt for a worker dispatch as a JSON object on stdout.
5
+ * Run by cts-worker subagents at the start of their turn to fetch the composed
6
+ * first-turn prompt for their dispatch-id (see the specify-codebase workflow).
7
+ */
8
+ import { composeDispatchContext } from "../../codebase-to-spec/dispatch.js";
9
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
10
+ export async function dispatchContextCommand(dispatchId) {
11
+ const ctx = composeDispatchContext(dispatchId);
12
+ if (!ctx) {
13
+ process.stderr.write(`Unknown dispatch-id: ${dispatchId}\n`);
14
+ process.exitCode = ExitCode.MissingInput;
15
+ return;
16
+ }
17
+ process.stdout.write(`${JSON.stringify(ctx)}\n`);
18
+ }
19
+ //# sourceMappingURL=dispatch-context.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-editor <area-prefix>` subcommand.
3
+ *
4
+ * Returns a dispatch payload for an editor pass on a specific area's partial.
5
+ * Validates that the area's `review.result` is `"needs-revision"` and that
6
+ * the partial file exists before scaffolding the dispatch.
7
+ *
8
+ * The worker overwrites the existing partial via Edit; the area.review.thread
9
+ * in outline.yaml stays where it is (the orchestrator manages it).
10
+ *
11
+ * Requirements covered:
12
+ * - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
13
+ */
14
+ export declare function dispatchEditorCommand(areaPrefix: string): Promise<void>;
15
+ //# sourceMappingURL=dispatch-editor.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-editor <area-prefix>` subcommand.
3
+ *
4
+ * Returns a dispatch payload for an editor pass on a specific area's partial.
5
+ * Validates that the area's `review.result` is `"needs-revision"` and that
6
+ * the partial file exists before scaffolding the dispatch.
7
+ *
8
+ * The worker overwrites the existing partial via Edit; the area.review.thread
9
+ * in outline.yaml stays where it is (the orchestrator manages it).
10
+ *
11
+ * Requirements covered:
12
+ * - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
13
+ */
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { cachePaths } from "../../codebase-to-spec/cache.js";
16
+ import { EDITOR_DISPATCH_ID_PREFIX } from "../../codebase-to-spec/dispatch.js";
17
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
18
+ import { sanitizeAreaName } from "../../codebase-to-spec/fan-out.js";
19
+ import { parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
20
+ import { findProjectRoot } from "../../utils/project-settings.js";
21
+ export async function dispatchEditorCommand(areaPrefix) {
22
+ const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
23
+ const paths = cachePaths(projectRoot);
24
+ if (!existsSync(paths.outline)) {
25
+ process.stderr.write(`No outline.yaml found at ${paths.outline}. Dispatch the planner first.\n`);
26
+ process.exitCode = ExitCode.MissingInput;
27
+ return;
28
+ }
29
+ let outline;
30
+ try {
31
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
32
+ }
33
+ catch (err) {
34
+ process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
35
+ process.exitCode = ExitCode.MissingInput;
36
+ return;
37
+ }
38
+ const area = outline.areas.find((a) => a.prefix === areaPrefix);
39
+ if (!area) {
40
+ process.stderr.write(`No area with prefix "${areaPrefix}" in outline.yaml. Available prefixes: ${outline.areas
41
+ .map((a) => a.prefix)
42
+ .join(", ")}\n`);
43
+ process.exitCode = ExitCode.MissingInput;
44
+ return;
45
+ }
46
+ if (!area.review) {
47
+ process.stderr.write(`Area "${areaPrefix}" has no review yet. Write a per-area review with result=needs-revision before dispatching the editor.\n`);
48
+ process.exitCode = ExitCode.MissingInput;
49
+ return;
50
+ }
51
+ if (area.review.result === "approved") {
52
+ process.stderr.write(`Area "${areaPrefix}" has review.result "approved" — nothing to revise. The partial is good as-is.\n`);
53
+ process.exitCode = ExitCode.MissingInput;
54
+ return;
55
+ }
56
+ const partialPath = paths.partial(sanitizeAreaName(area.name));
57
+ if (!existsSync(partialPath)) {
58
+ process.stderr.write(`Partial not found at ${partialPath}. Run the specifier for area "${areaPrefix}" before dispatching the editor.\n`);
59
+ process.exitCode = ExitCode.MissingInput;
60
+ return;
61
+ }
62
+ const payload = {
63
+ dispatch_id: `${EDITOR_DISPATCH_ID_PREFIX}${areaPrefix}`,
64
+ output_path: partialPath,
65
+ area_name: area.name,
66
+ area_prefix: area.prefix,
67
+ };
68
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
69
+ }
70
+ //# sourceMappingURL=dispatch-editor.js.map