@popoverai/dotrequirements 0.24.1 → 0.24.3

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 (53) hide show
  1. package/README.md +1 -1
  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/claude.d.ts +1 -0
  5. package/dist/codebase-to-spec/claude.js +9 -0
  6. package/dist/codebase-to-spec/dispatch.d.ts +69 -0
  7. package/dist/codebase-to-spec/dispatch.js +484 -0
  8. package/dist/codebase-to-spec/pack.d.ts +16 -0
  9. package/dist/codebase-to-spec/pack.js +17 -3
  10. package/dist/codebase-to-spec/present.d.ts +8 -1
  11. package/dist/codebase-to-spec/present.js +7 -4
  12. package/dist/codebase-to-spec/progress.d.ts +6 -0
  13. package/dist/codebase-to-spec/progress.js +34 -0
  14. package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +1 -1
  15. package/dist/codebase-to-spec/prompts/outline-reviewer.js +3 -1
  16. package/dist/codebase-to-spec/prompts/planner-initial.d.ts +1 -1
  17. package/dist/codebase-to-spec/prompts/planner-initial.js +4 -0
  18. package/dist/codebase-to-spec/prompts/planner-revise.d.ts +1 -1
  19. package/dist/codebase-to-spec/prompts/planner-revise.js +2 -2
  20. package/dist/codebase-to-spec/prompts/spec-reviewer.d.ts +1 -1
  21. package/dist/codebase-to-spec/prompts/spec-reviewer.js +6 -1
  22. package/dist/codebase-to-spec/prompts/specifier.d.ts +1 -1
  23. package/dist/codebase-to-spec/prompts/specifier.js +6 -4
  24. package/dist/codebase-to-spec/prompts/style-check.d.ts +10 -2
  25. package/dist/codebase-to-spec/prompts/style-check.js +76 -46
  26. package/dist/codebase-to-spec/schemas.d.ts +460 -1
  27. package/dist/codebase-to-spec/schemas.js +158 -1
  28. package/dist/codebase-to-spec/skill-install.d.ts +36 -12
  29. package/dist/codebase-to-spec/skill-install.js +127 -26
  30. package/dist/codebase-to-spec/specifier.js +6 -0
  31. package/dist/commands/codebase-to-spec/compose-orchestrator.d.ts +14 -0
  32. package/dist/commands/codebase-to-spec/compose-orchestrator.js +54 -0
  33. package/dist/commands/codebase-to-spec/dispatch-context.d.ts +12 -0
  34. package/dist/commands/codebase-to-spec/dispatch-context.js +22 -0
  35. package/dist/commands/codebase-to-spec/dispatch-editor.d.ts +16 -0
  36. package/dist/commands/codebase-to-spec/dispatch-editor.js +71 -0
  37. package/dist/commands/codebase-to-spec/dispatch-planner.d.ts +19 -0
  38. package/dist/commands/codebase-to-spec/dispatch-planner.js +90 -0
  39. package/dist/commands/codebase-to-spec/dispatch-spec.d.ts +16 -0
  40. package/dist/commands/codebase-to-spec/dispatch-spec.js +59 -0
  41. package/dist/commands/codebase-to-spec/index.js +69 -1
  42. package/dist/commands/codebase-to-spec/pack.d.ts +6 -0
  43. package/dist/commands/codebase-to-spec/pack.js +1 -0
  44. package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +20 -0
  45. package/dist/commands/codebase-to-spec/present-orchestrator.js +81 -0
  46. package/dist/commands/codebase-to-spec/present.d.ts +5 -0
  47. package/dist/commands/codebase-to-spec/present.js +6 -1
  48. package/dist/commands/codebase-to-spec/run.js +1 -0
  49. package/dist/commands/codebase-to-spec/skill-install.js +12 -1
  50. package/dist/templates/agents/cts-worker.md +9 -0
  51. package/dist/templates/hooks/cts-worker-persona.sh +76 -0
  52. package/dist/templates/skills/codebase-to-spec/SKILL.md +159 -68
  53. package/package.json +4 -5
@@ -10,6 +10,10 @@
10
10
  */
11
11
  import { z } from "zod";
12
12
  // ---------- Outline ----------
13
+ export const CustomerSchema = z.object({
14
+ name: z.string().min(1),
15
+ description: z.string().min(1),
16
+ });
13
17
  export const AreaSchema = z.object({
14
18
  name: z.string().min(1),
15
19
  description: z.string().min(1),
@@ -17,6 +21,13 @@ export const AreaSchema = z.object({
17
21
  .string()
18
22
  .regex(/^[A-Z][A-Z0-9_]*$/, "prefix must be uppercase alphanumeric/underscore"),
19
23
  files: z.array(z.string()).min(0),
24
+ /**
25
+ * Customers this area serves — at least one. Each customer is a *user* of
26
+ * the software (not a contributor to its codebase). The specifier will use
27
+ * one of these as the persona it grounds the area's requirements in.
28
+ * See CTS-PLAN-1 (customer threading).
29
+ */
30
+ customers: z.array(CustomerSchema).min(1),
20
31
  });
21
32
  export const OutlineSchema = z.object({
22
33
  title: z.string().min(1),
@@ -45,8 +56,21 @@ export const OUTLINE_JSON_SCHEMA = {
45
56
  description: { type: "string" },
46
57
  prefix: { type: "string" },
47
58
  files: { type: "array", items: { type: "string" } },
59
+ customers: {
60
+ type: "array",
61
+ minItems: 1,
62
+ items: {
63
+ type: "object",
64
+ properties: {
65
+ name: { type: "string" },
66
+ description: { type: "string" },
67
+ },
68
+ required: ["name", "description"],
69
+ additionalProperties: false,
70
+ },
71
+ },
48
72
  },
49
- required: ["name", "description", "prefix", "files"],
73
+ required: ["name", "description", "prefix", "files", "customers"],
50
74
  additionalProperties: false,
51
75
  },
52
76
  },
@@ -165,6 +189,139 @@ export const SPEC_REVIEW_JSON_SCHEMA = {
165
189
  ],
166
190
  additionalProperties: false,
167
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
+ review: ConversationalReviewSchema.optional(),
256
+ areas: z.array(ConversationalAreaSchema).min(1),
257
+ });
258
+ /**
259
+ * Parse YAML text against the conversational outline schema. Throws with
260
+ * a descriptive message on parse or validation failure.
261
+ */
262
+ export function parseConversationalOutline(text) {
263
+ let raw;
264
+ try {
265
+ raw = parseYaml(text);
266
+ }
267
+ catch (err) {
268
+ throw new Error(`Outline YAML is invalid: ${err instanceof Error ? err.message : String(err)}`);
269
+ }
270
+ return ConversationalOutlineSchema.parse(raw);
271
+ }
272
+ /**
273
+ * Synthesize a short customer `name` from a conversational customer's
274
+ * `description`. The orchestrator's `ConversationalCustomerSchema` only
275
+ * carries description, but the legacy `CustomerSchema` requires both
276
+ * `name` and `description`. Adapter use only — downstream compose/present
277
+ * stages don't read `name`, so this is a schema-shape bridge, not a
278
+ * semantic field.
279
+ *
280
+ * Heuristic: descriptions like "Priya, a CTS pipeline operator" yield
281
+ * "Priya" via first comma-segment. Otherwise truncate to 40 chars.
282
+ */
283
+ function synthesizeCustomerName(description) {
284
+ const firstSegment = description.split(",")[0]?.trim() ?? "";
285
+ if (firstSegment.length > 0 && firstSegment.length <= 40) {
286
+ return firstSegment;
287
+ }
288
+ return description.slice(0, 40).trim() || description;
289
+ }
290
+ /**
291
+ * Adapt a conversational orchestrator outline to the legacy Outline shape
292
+ * for use with stages that still take the legacy schema (compose, present).
293
+ *
294
+ * Discards orchestrator-only fields (review); maps `source_files` → `files`;
295
+ * bridges the customer-shape difference by synthesizing a `name` from each
296
+ * conversational customer's description.
297
+ */
298
+ export function conversationalOutlineToLegacy(outline) {
299
+ return {
300
+ title: outline.title,
301
+ defaultPrefix: outline.defaultPrefix,
302
+ summary: outline.summary,
303
+ areas: outline.areas.map((a) => ({
304
+ name: a.name,
305
+ description: a.description,
306
+ prefix: a.prefix,
307
+ files: a.source_files,
308
+ customers: a.customers.map((c) => ({
309
+ name: synthesizeCustomerName(c.description),
310
+ description: c.description,
311
+ })),
312
+ })),
313
+ };
314
+ }
315
+ /**
316
+ * Serialize a conversational outline back to YAML text. Uses literal-block
317
+ * multi-line strings (`|`) where possible for readability.
318
+ */
319
+ export function stringifyConversationalOutline(outline) {
320
+ return stringifyYaml(outline, {
321
+ lineWidth: 0,
322
+ blockQuote: "literal",
323
+ });
324
+ }
168
325
  export function parseSpecReview(text) {
169
326
  const trimmed = text.trim();
170
327
  let raw;
@@ -1,34 +1,39 @@
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 + persona-
5
+ * injection hook script + hook registration in `.claude/settings.local.json`.
6
+ * The skill body (the conversational orchestrator) DEPENDS on the companion
7
+ * files — installing just the skill without them would leave Task dispatches
8
+ * unable to resolve their persona. For `project` scope, all four pieces are
9
+ * installed in one shot. For `global` and `custom` scopes, only the skill
10
+ * body is installed; the companions are project-scoped by CC convention.
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-CLI-1 (bundles the agent + hook that PreToolUse-injects persona bodies)
17
19
  */
18
20
  export type SkillInstallScope = "project" | "global" | "custom";
19
21
  export interface SkillInstallOptions {
20
22
  /**
21
23
  * Where to install:
22
24
  * - `project` (default): `<projectRoot>/.claude/skills/codebase-to-spec/`
23
- * - `global`: `~/.claude/skills/codebase-to-spec/`
24
- * - `custom`: requires `targetDir`
25
+ * (plus companions under `<projectRoot>/.claude/agents/` and
26
+ * `<projectRoot>/.claude/hooks/`, and hook registration in
27
+ * `<projectRoot>/.claude/settings.local.json`)
28
+ * - `global`: `~/.claude/skills/codebase-to-spec/` (skill body only)
29
+ * - `custom`: requires `targetDir` (skill body only)
25
30
  */
26
31
  scope?: SkillInstallScope;
27
32
  /** Required when `scope === 'custom'`. The skill directory will be created here. */
28
33
  targetDir?: string;
29
34
  /** Project root (used when scope === 'project'). Defaults to cwd. */
30
35
  projectRoot?: string;
31
- /** Overwrite existing SKILL.md if present. Default false. */
36
+ /** Overwrite existing SKILL.md / agent / hook if present. Default false. */
32
37
  overwrite?: boolean;
33
38
  }
34
39
  export interface SkillInstallResult {
@@ -38,6 +43,13 @@ export interface SkillInstallResult {
38
43
  installed: boolean;
39
44
  /** True if an existing file was replaced. */
40
45
  overwrote: boolean;
46
+ /** Companion files installed alongside the skill (project scope only). */
47
+ companions?: {
48
+ agentPath: string;
49
+ hookScriptPath: string;
50
+ settingsPath: string;
51
+ hookRegistered: boolean;
52
+ };
41
53
  }
42
54
  /**
43
55
  * Resolve the directory in which the skill should live, given the user's
@@ -50,8 +62,20 @@ export declare function resolveSkillDir(options: SkillInstallOptions): string;
50
62
  */
51
63
  export declare function loadSkillTemplate(): string;
52
64
  /**
53
- * Install (or refuse to overwrite) the codebase-to-spec SKILL.md at the
54
- * resolved location.
65
+ * Load the bundled cts-worker agent definition template.
66
+ */
67
+ export declare function loadAgentTemplate(): string;
68
+ /**
69
+ * Load the bundled persona-injection hook script template.
70
+ */
71
+ export declare function loadHookTemplate(): string;
72
+ /**
73
+ * Install (or refuse to overwrite) the codebase-to-spec skill bundle.
74
+ *
75
+ * For `project` scope: installs skill body, cts-worker agent, persona hook
76
+ * script, and registers the hook in settings.local.json.
77
+ *
78
+ * For `global` and `custom` scopes: installs only the skill body.
55
79
  */
56
80
  export declare function installSkill(options?: SkillInstallOptions): SkillInstallResult;
57
81
  //# sourceMappingURL=skill-install.d.ts.map
@@ -1,21 +1,23 @@
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 + persona-
5
+ * injection hook script + hook registration in `.claude/settings.local.json`.
6
+ * The skill body (the conversational orchestrator) DEPENDS on the companion
7
+ * files — installing just the skill without them would leave Task dispatches
8
+ * unable to resolve their persona. For `project` scope, all four pieces are
9
+ * installed in one shot. For `global` and `custom` scopes, only the skill
10
+ * body is installed; the companions are project-scoped by CC convention.
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-CLI-1 (bundles the agent + hook that PreToolUse-injects persona bodies)
17
19
  */
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
19
21
  import { homedir } from "node:os";
20
22
  import { dirname, join, resolve } from "node:path";
21
23
  import { loadTemplate } from "../utils/templates.js";
@@ -49,31 +51,130 @@ 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 persona-injection hook script template.
61
+ */
62
+ export function loadHookTemplate() {
63
+ return loadTemplate("hooks/cts-worker-persona.sh");
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
+ * Register the cts-worker-persona PreToolUse hook in the project's
84
+ * `.claude/settings.local.json`, preserving any existing keys.
85
+ *
86
+ * Returns true if the hook registration was added or updated.
87
+ */
88
+ function registerHookInSettings(projectRoot, hookScriptPath) {
89
+ const settingsPath = join(projectRoot, ".claude", "settings.local.json");
90
+ const hookEntry = {
91
+ matcher: "Task",
92
+ hooks: [{ type: "command", command: hookScriptPath }],
93
+ };
94
+ // Read existing settings if present.
95
+ let settings = {};
96
+ if (existsSync(settingsPath)) {
97
+ try {
98
+ const content = readFileSync(settingsPath, "utf-8");
99
+ if (content.trim()) {
100
+ settings = JSON.parse(content);
101
+ }
102
+ }
103
+ catch {
104
+ // Invalid JSON — refuse to clobber. Tell caller to fix manually.
105
+ throw new Error(`Existing ${settingsPath} contains invalid JSON. Fix it or delete it before installing the orchestrator hook.`);
106
+ }
107
+ }
108
+ // Initialize the hooks tree if missing.
109
+ // biome-ignore lint/suspicious/noExplicitAny: settings.local.json is user-owned; we narrow ad-hoc.
110
+ const hooks = (settings.hooks ?? {});
111
+ const preToolUse = (hooks.PreToolUse ?? []);
112
+ // Check if an entry already references our hook script (idempotent).
113
+ const alreadyRegistered = preToolUse.some((entry) => {
114
+ if (entry.matcher !== "Task")
115
+ return false;
116
+ const innerHooks = entry.hooks;
117
+ if (!Array.isArray(innerHooks))
118
+ return false;
119
+ return innerHooks.some((h) => h.command === hookScriptPath);
120
+ });
121
+ if (alreadyRegistered) {
122
+ return { settingsPath, registered: false };
123
+ }
124
+ preToolUse.push(hookEntry);
125
+ hooks.PreToolUse = preToolUse;
126
+ settings.hooks = hooks;
127
+ mkdirSync(dirname(settingsPath), { recursive: true });
128
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
129
+ return { settingsPath, registered: true };
130
+ }
131
+ /**
132
+ * Install (or refuse to overwrite) the codebase-to-spec skill bundle.
133
+ *
134
+ * For `project` scope: installs skill body, cts-worker agent, persona hook
135
+ * script, and registers the hook in settings.local.json.
136
+ *
137
+ * For `global` and `custom` scopes: installs only the skill body.
54
138
  */
55
139
  export function installSkill(options = {}) {
56
140
  const skillDir = resolveSkillDir(options);
57
141
  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.`);
142
+ const skillTemplate = loadSkillTemplate();
143
+ const skillResult = installTemplateFile(skillTemplate, skillPath, options.overwrite === true, "SKILL.md");
144
+ // Skill body only for global/custom scopes — CC reads agents and hooks
145
+ // per-project, so the companions don't make sense outside a project.
146
+ const scope = options.scope ?? "project";
147
+ if (scope !== "project") {
148
+ return {
149
+ installedPath: skillPath,
150
+ installed: skillResult.installed,
151
+ overwrote: skillResult.overwrote,
152
+ };
70
153
  }
71
- mkdirSync(dirname(skillPath), { recursive: true });
72
- writeFileSync(skillPath, template, "utf-8");
154
+ const projectRoot = options.projectRoot ?? process.cwd();
155
+ const agentPath = join(projectRoot, ".claude", "agents", "cts-worker.md");
156
+ const hookScriptPath = join(projectRoot, ".claude", "hooks", "cts-worker-persona.sh");
157
+ // Install the cts-worker agent definition.
158
+ installTemplateFile(loadAgentTemplate(), agentPath, options.overwrite === true, "cts-worker agent definition");
159
+ // Install the persona-injection hook script (executable bit set separately).
160
+ installTemplateFile(loadHookTemplate(), hookScriptPath, options.overwrite === true, "cts-worker-persona hook script");
161
+ // chmod the hook script to be executable so Claude Code's PreToolUse
162
+ // hook can invoke it. (Imported statically — the CLI package is ESM, so
163
+ // `require("node:fs")` here would throw ReferenceError and silently
164
+ // leave the script at 0644, breaking the hook on every install.)
165
+ chmodSync(hookScriptPath, 0o755);
166
+ // Register the hook in settings.local.json.
167
+ const { settingsPath, registered } = registerHookInSettings(projectRoot, hookScriptPath);
73
168
  return {
74
169
  installedPath: skillPath,
75
- installed: true,
76
- overwrote: exists,
170
+ installed: skillResult.installed,
171
+ overwrote: skillResult.overwrote,
172
+ companions: {
173
+ agentPath,
174
+ hookScriptPath,
175
+ settingsPath,
176
+ hookRegistered: registered,
177
+ },
77
178
  };
78
179
  }
79
180
  //# sourceMappingURL=skill-install.js.map
@@ -24,6 +24,12 @@ function buildUserMessage(ctx) {
24
24
  `Area description: ${ctx.area.description}`,
25
25
  `Document defaultPrefix: ${ctx.outline.defaultPrefix}`,
26
26
  `Area prefix: ${ctx.area.prefix}`,
27
+ ``,
28
+ `## Customers this area serves`,
29
+ `(Pick one of these as the named persona for your requirements. Do not invent a different customer. If none of these is a real user of the software — e.g., all entries describe a contributor to this codebase — follow the AREA-LACKS-CUSTOMER escape in your system prompt instead of writing requirements.)`,
30
+ `\`\`\`json`,
31
+ JSON.stringify(ctx.area.customers, null, 2),
32
+ `\`\`\``,
27
33
  `Use full requirement IDs of the form: ${ctx.outline.defaultPrefix}-${ctx.area.prefix}-1, ${ctx.outline.defaultPrefix}-${ctx.area.prefix}-2, etc. (sequential, 1-indexed, no zero-padding).`,
28
34
  ``,
29
35
  `## Paths`,
@@ -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,12 @@
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
+ * Called by the cts-worker PreToolUse hook to compose the subagent's first-turn
6
+ * prompt via `modifiedInput`.
7
+ *
8
+ * Requirements covered:
9
+ * - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
10
+ */
11
+ export declare function dispatchContextCommand(dispatchId: string): Promise<void>;
12
+ //# sourceMappingURL=dispatch-context.d.ts.map
@@ -0,0 +1,22 @@
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
+ * Called by the cts-worker PreToolUse hook to compose the subagent's first-turn
6
+ * prompt via `modifiedInput`.
7
+ *
8
+ * Requirements covered:
9
+ * - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
10
+ */
11
+ import { composeDispatchContext } from "../../codebase-to-spec/dispatch.js";
12
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
13
+ export async function dispatchContextCommand(dispatchId) {
14
+ const ctx = composeDispatchContext(dispatchId);
15
+ if (!ctx) {
16
+ process.stderr.write(`Unknown dispatch-id: ${dispatchId}\n`);
17
+ process.exitCode = ExitCode.MissingInput;
18
+ return;
19
+ }
20
+ process.stdout.write(`${JSON.stringify(ctx)}\n`);
21
+ }
22
+ //# sourceMappingURL=dispatch-context.js.map
@@ -0,0 +1,16 @@
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-CLI-1: CLI exposes commands that return dispatch instructions
13
+ * - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
14
+ */
15
+ export declare function dispatchEditorCommand(areaPrefix: string): Promise<void>;
16
+ //# sourceMappingURL=dispatch-editor.d.ts.map
@@ -0,0 +1,71 @@
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-CLI-1: CLI exposes commands that return dispatch instructions
13
+ * - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
14
+ */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { cachePaths } from "../../codebase-to-spec/cache.js";
17
+ import { EDITOR_DISPATCH_ID_PREFIX } from "../../codebase-to-spec/dispatch.js";
18
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
19
+ import { sanitizeAreaName } from "../../codebase-to-spec/fan-out.js";
20
+ import { parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
21
+ import { findProjectRoot } from "../../utils/project-settings.js";
22
+ export async function dispatchEditorCommand(areaPrefix) {
23
+ const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
24
+ const paths = cachePaths(projectRoot);
25
+ if (!existsSync(paths.outline)) {
26
+ process.stderr.write(`No outline.yaml found at ${paths.outline}. Dispatch the planner first.\n`);
27
+ process.exitCode = ExitCode.MissingInput;
28
+ return;
29
+ }
30
+ let outline;
31
+ try {
32
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
33
+ }
34
+ catch (err) {
35
+ process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
36
+ process.exitCode = ExitCode.MissingInput;
37
+ return;
38
+ }
39
+ const area = outline.areas.find((a) => a.prefix === areaPrefix);
40
+ if (!area) {
41
+ process.stderr.write(`No area with prefix "${areaPrefix}" in outline.yaml. Available prefixes: ${outline.areas
42
+ .map((a) => a.prefix)
43
+ .join(", ")}\n`);
44
+ process.exitCode = ExitCode.MissingInput;
45
+ return;
46
+ }
47
+ if (!area.review) {
48
+ process.stderr.write(`Area "${areaPrefix}" has no review yet. Write a per-area review with result=needs-revision before dispatching the editor.\n`);
49
+ process.exitCode = ExitCode.MissingInput;
50
+ return;
51
+ }
52
+ if (area.review.result === "approved") {
53
+ process.stderr.write(`Area "${areaPrefix}" has review.result "approved" — nothing to revise. The partial is good as-is.\n`);
54
+ process.exitCode = ExitCode.MissingInput;
55
+ return;
56
+ }
57
+ const partialPath = paths.partial(sanitizeAreaName(area.name));
58
+ if (!existsSync(partialPath)) {
59
+ process.stderr.write(`Partial not found at ${partialPath}. Run the specifier for area "${areaPrefix}" before dispatching the editor.\n`);
60
+ process.exitCode = ExitCode.MissingInput;
61
+ return;
62
+ }
63
+ const payload = {
64
+ dispatch_id: `${EDITOR_DISPATCH_ID_PREFIX}${areaPrefix}`,
65
+ output_path: partialPath,
66
+ area_name: area.name,
67
+ area_prefix: area.prefix,
68
+ };
69
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
70
+ }
71
+ //# sourceMappingURL=dispatch-editor.js.map