@popoverai/dotrequirements 0.24.2 → 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 (27) hide show
  1. package/README.md +0 -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/dispatch.d.ts +69 -0
  5. package/dist/codebase-to-spec/dispatch.js +484 -0
  6. package/dist/codebase-to-spec/schemas.d.ts +375 -0
  7. package/dist/codebase-to-spec/schemas.js +133 -0
  8. package/dist/codebase-to-spec/skill-install.d.ts +36 -12
  9. package/dist/codebase-to-spec/skill-install.js +127 -26
  10. package/dist/commands/codebase-to-spec/compose-orchestrator.d.ts +14 -0
  11. package/dist/commands/codebase-to-spec/compose-orchestrator.js +54 -0
  12. package/dist/commands/codebase-to-spec/dispatch-context.d.ts +12 -0
  13. package/dist/commands/codebase-to-spec/dispatch-context.js +22 -0
  14. package/dist/commands/codebase-to-spec/dispatch-editor.d.ts +16 -0
  15. package/dist/commands/codebase-to-spec/dispatch-editor.js +71 -0
  16. package/dist/commands/codebase-to-spec/dispatch-planner.d.ts +19 -0
  17. package/dist/commands/codebase-to-spec/dispatch-planner.js +90 -0
  18. package/dist/commands/codebase-to-spec/dispatch-spec.d.ts +16 -0
  19. package/dist/commands/codebase-to-spec/dispatch-spec.js +59 -0
  20. package/dist/commands/codebase-to-spec/index.js +56 -1
  21. package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +20 -0
  22. package/dist/commands/codebase-to-spec/present-orchestrator.js +81 -0
  23. package/dist/commands/codebase-to-spec/skill-install.js +12 -1
  24. package/dist/templates/agents/cts-worker.md +9 -0
  25. package/dist/templates/hooks/cts-worker-persona.sh +76 -0
  26. package/dist/templates/skills/codebase-to-spec/SKILL.md +159 -68
  27. package/package.json +1 -1
@@ -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
@@ -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
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-planner [--revise]` subcommand.
3
+ *
4
+ * Initial mode: returns a dispatch payload for the first planner pass.
5
+ *
6
+ * --revise mode: reads the current `outline.yaml`, validates that its
7
+ * `review.result` is "needs-revision", and returns a payload for the next
8
+ * revise dispatch. The worker overwrites outline.yaml's content while
9
+ * preserving the review section.
10
+ *
11
+ * Requirements covered:
12
+ * - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
13
+ * - CTSO-CONV-2: orchestrator iterates on planner's outline until approved
14
+ */
15
+ export interface DispatchPlannerOptions {
16
+ revise?: boolean;
17
+ }
18
+ export declare function dispatchPlannerCommand(options?: DispatchPlannerOptions): Promise<void>;
19
+ //# sourceMappingURL=dispatch-planner.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-planner [--revise]` subcommand.
3
+ *
4
+ * Initial mode: returns a dispatch payload for the first planner pass.
5
+ *
6
+ * --revise mode: reads the current `outline.yaml`, validates that its
7
+ * `review.result` is "needs-revision", and returns a payload for the next
8
+ * revise dispatch. The worker overwrites outline.yaml's content while
9
+ * preserving the review section.
10
+ *
11
+ * Requirements covered:
12
+ * - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
13
+ * - CTSO-CONV-2: orchestrator iterates on planner's outline until approved
14
+ */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { cachePaths } from "../../codebase-to-spec/cache.js";
17
+ import { PLANNER_INITIAL_DISPATCH_ID, PLANNER_REVISE_DISPATCH_ID_PREFIX, } from "../../codebase-to-spec/dispatch.js";
18
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
19
+ import { parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
20
+ import { findProjectRoot } from "../../utils/project-settings.js";
21
+ export async function dispatchPlannerCommand(options = {}) {
22
+ const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
23
+ const paths = cachePaths(projectRoot);
24
+ if (options.revise) {
25
+ return dispatchPlannerRevise(paths);
26
+ }
27
+ return dispatchPlannerInitial(paths);
28
+ }
29
+ function dispatchPlannerInitial(paths) {
30
+ if (!existsSync(paths.overview)) {
31
+ process.stderr.write(`Compressed pack not found at ${paths.overview}. Run \`dotrequirements cts pack\` first.\n`);
32
+ process.exitCode = ExitCode.MissingInput;
33
+ return;
34
+ }
35
+ const payload = {
36
+ dispatch_id: PLANNER_INITIAL_DISPATCH_ID,
37
+ output_path: paths.outline,
38
+ };
39
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
40
+ }
41
+ function dispatchPlannerRevise(paths) {
42
+ if (!existsSync(paths.outline)) {
43
+ process.stderr.write(`No outline.yaml found at ${paths.outline}. Dispatch the initial planner first.\n`);
44
+ process.exitCode = ExitCode.MissingInput;
45
+ return;
46
+ }
47
+ // Parse and validate the current outline. Surface clear errors rather
48
+ // than letting a malformed file silently produce a bad revise dispatch.
49
+ let outline;
50
+ try {
51
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
52
+ }
53
+ catch (err) {
54
+ process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
55
+ process.exitCode = ExitCode.MissingInput;
56
+ return;
57
+ }
58
+ // The orchestrator should only call --revise when the outline has been
59
+ // reviewed and the result is "needs-revision". Other states are user
60
+ // errors worth surfacing clearly.
61
+ if (!outline.review) {
62
+ process.stderr.write(`Outline at ${paths.outline} has no \`review\` section. Add CA's review (with result and thread) before dispatching revise.\n`);
63
+ process.exitCode = ExitCode.MissingInput;
64
+ return;
65
+ }
66
+ if (outline.review.result === "approved") {
67
+ process.stderr.write(`Outline at ${paths.outline} has review.result "approved" — nothing to revise. Proceed to fan-out.\n`);
68
+ process.exitCode = ExitCode.MissingInput;
69
+ return;
70
+ }
71
+ // Latest thread entry must be needs-revision for there to be revisions
72
+ // to act on. Schema's discriminated union enforces revisions ≥ 1 when
73
+ // the entry is needs-revision.
74
+ const latestEntry = outline.review.thread[outline.review.thread.length - 1];
75
+ if (!latestEntry || latestEntry.result !== "needs-revision") {
76
+ process.stderr.write(`Outline at ${paths.outline} has review.result "needs-revision" but the latest thread entry isn't a needs-revision entry. The review thread is malformed.\n`);
77
+ process.exitCode = ExitCode.MissingInput;
78
+ return;
79
+ }
80
+ // Next turn = current thread length + 1 (the round being PRODUCED by
81
+ // this revise dispatch; CA's review of THAT output will append a new
82
+ // thread entry of length+1 after this dispatch completes).
83
+ const nextTurn = outline.review.thread.length + 1;
84
+ const payload = {
85
+ dispatch_id: `${PLANNER_REVISE_DISPATCH_ID_PREFIX}${nextTurn}`,
86
+ output_path: paths.outline,
87
+ };
88
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
89
+ }
90
+ //# sourceMappingURL=dispatch-planner.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-spec` subcommand.
3
+ *
4
+ * Returns an array of specifier dispatch payloads — one per area in the
5
+ * approved outline. The conversational orchestrator reads this, fires N
6
+ * background Task dispatches in parallel (one cts-worker per area), and
7
+ * collects the resulting partials.
8
+ *
9
+ * Errors clearly if `outline.yaml`'s `review.result` isn't `"approved"` —
10
+ * the orchestrator should approve the outline before fan-out.
11
+ *
12
+ * Requirements covered:
13
+ * - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
14
+ */
15
+ export declare function dispatchSpecCommand(): Promise<void>;
16
+ //# sourceMappingURL=dispatch-spec.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec dispatch-spec` subcommand.
3
+ *
4
+ * Returns an array of specifier dispatch payloads — one per area in the
5
+ * approved outline. The conversational orchestrator reads this, fires N
6
+ * background Task dispatches in parallel (one cts-worker per area), and
7
+ * collects the resulting partials.
8
+ *
9
+ * Errors clearly if `outline.yaml`'s `review.result` isn't `"approved"` —
10
+ * the orchestrator should approve the outline before fan-out.
11
+ *
12
+ * Requirements covered:
13
+ * - CTSO-CLI-1: CLI exposes commands that return dispatch instructions
14
+ */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { cachePaths } from "../../codebase-to-spec/cache.js";
17
+ import { SPECIFIER_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 dispatchSpecCommand() {
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
+ if (!outline.review) {
40
+ process.stderr.write(`Outline at ${paths.outline} has no \`review\` section. Add CA's review with result=approved before dispatching specifiers.\n`);
41
+ process.exitCode = ExitCode.MissingInput;
42
+ return;
43
+ }
44
+ if (outline.review.result !== "approved") {
45
+ process.stderr.write(`Outline at ${paths.outline} has review.result "${outline.review.result}", not "approved". Approve the outline before dispatching specifiers.\n`);
46
+ process.exitCode = ExitCode.MissingInput;
47
+ return;
48
+ }
49
+ // For each area, produce a dispatch payload. The skill body fires N
50
+ // background Task calls in parallel using these payloads.
51
+ const dispatches = outline.areas.map((area) => ({
52
+ dispatch_id: `${SPECIFIER_DISPATCH_ID_PREFIX}${area.prefix}`,
53
+ output_path: paths.partial(sanitizeAreaName(area.name)),
54
+ area_name: area.name,
55
+ area_prefix: area.prefix,
56
+ }));
57
+ process.stdout.write(`${JSON.stringify(dispatches)}\n`);
58
+ }
59
+ //# sourceMappingURL=dispatch-spec.js.map
@@ -6,16 +6,31 @@
6
6
  */
7
7
  import { installCtsAbortHandlers } from "../../codebase-to-spec/progress.js";
8
8
  import { composeCommand } from "./compose.js";
9
+ import { composeOrchestratorCommand } from "./compose-orchestrator.js";
10
+ import { dispatchContextCommand } from "./dispatch-context.js";
11
+ import { dispatchEditorCommand } from "./dispatch-editor.js";
12
+ import { dispatchPlannerCommand } from "./dispatch-planner.js";
13
+ import { dispatchSpecCommand } from "./dispatch-spec.js";
9
14
  import { editLoopCommand } from "./edit-loop.js";
10
15
  import { fanOutCommand } from "./fan-out.js";
11
16
  import { packCommand } from "./pack.js";
12
17
  import { planLoopCommand } from "./plan-loop.js";
13
18
  import { presentCommand } from "./present.js";
19
+ import { presentOrchestratorCommand } from "./present-orchestrator.js";
14
20
  import { runCommand } from "./run.js";
15
21
  import { skillInstallCommand } from "./skill-install.js";
16
22
  import { specifyAreaCommand } from "./specify-area.js";
17
23
  import { styleCheckCommand } from "./style-check.js";
18
24
  import { validateCommand } from "./validate.js";
25
+ /**
26
+ * Marker used to hide a command from `--help` output without removing it.
27
+ * Used for dark-ship: the conversational orchestrator's plumbing commands and
28
+ * the skill-install entry point ship in the npm package but aren't surfaced
29
+ * in help listings or documentation. They remain fully invocable (the
30
+ * orchestrator skill calls them by name) — they just don't pollute the
31
+ * user-facing CLI surface until we flip the default path in Phase 6.
32
+ */
33
+ const HIDDEN = { hidden: true };
19
34
  export function registerCodebaseToSpec(program) {
20
35
  const cts = program
21
36
  .command("codebase-to-spec")
@@ -134,7 +149,47 @@ export function registerCodebaseToSpec(program) {
134
149
  });
135
150
  });
136
151
  cts
137
- .command("skill-install")
152
+ .command("dispatch-context <dispatch-id>", HIDDEN)
153
+ .description("Return the composed prompt for a worker dispatch as JSON on stdout (called by the cts-worker PreToolUse hook)")
154
+ .action((dispatchId) => dispatchContextCommand(dispatchId));
155
+ cts
156
+ .command("dispatch-planner", HIDDEN)
157
+ .description("Return a dispatch payload for a planner pass (initial, or --revise to scaffold the next turn from the latest cached outline + critique)")
158
+ .option("--revise", "Scaffold a revise dispatch using the latest cached outline + sibling critique file")
159
+ .action((opts) => dispatchPlannerCommand({ revise: opts.revise }));
160
+ cts
161
+ .command("dispatch-spec", HIDDEN)
162
+ .description("Return an array of specifier dispatch payloads — one per area in the approved outline (used by the conversational orchestrator skill to fan out N specifiers in parallel)")
163
+ .action(() => dispatchSpecCommand());
164
+ cts
165
+ .command("dispatch-editor <area-prefix>", HIDDEN)
166
+ .description("Return an editor dispatch payload for one area whose review is needs-revision (used by the conversational orchestrator skill to drive per-area convergence)")
167
+ .action((areaPrefix) => dispatchEditorCommand(areaPrefix));
168
+ cts
169
+ .command("compose-orchestrator", HIDDEN)
170
+ .description("Compose-orchestrator variant: reads outline.yaml (approved) + partials/, writes composedSpec")
171
+ .action(() => composeOrchestratorCommand());
172
+ cts
173
+ .command("present-orchestrator", HIDDEN)
174
+ .description("Present-orchestrator variant: reads outline.yaml + composedSpec, writes final files to .requirements/")
175
+ .option("--interactive", "Force interactive mode (override TTY detection)")
176
+ .option("--non-interactive", "Force non-interactive mode (override TTY detection)")
177
+ .option("--overwrite", "Replace existing .requirements/ files without prompting")
178
+ .option("--skip-existing", "Leave existing .requirements/ files untouched")
179
+ .action((opts) => {
180
+ const forceMode = opts.interactive
181
+ ? "interactive"
182
+ : opts.nonInteractive
183
+ ? "non-interactive"
184
+ : undefined;
185
+ return presentOrchestratorCommand({
186
+ forceMode,
187
+ overwrite: opts.overwrite,
188
+ skipExisting: opts.skipExisting,
189
+ });
190
+ });
191
+ cts
192
+ .command("skill-install", HIDDEN)
138
193
  .description("Install the codebase-to-spec skill (SKILL.md) into the host's skills directory")
139
194
  .option("--global", "Install to ~/.claude/skills/ instead of the project")
140
195
  .option("--target-dir <path>", "Install into a custom skills directory (for non-Claude-Code hosts)")