@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
@@ -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
@@ -4,28 +4,52 @@
4
4
  * Subcommands route to focused handlers in this directory. Each subcommand can
5
5
  * be invoked granularly (e.g., `dotrequirements cts pack`) per CTS-CLI-2.
6
6
  */
7
+ import { installCtsAbortHandlers } from "../../codebase-to-spec/progress.js";
7
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";
8
14
  import { editLoopCommand } from "./edit-loop.js";
9
15
  import { fanOutCommand } from "./fan-out.js";
10
16
  import { packCommand } from "./pack.js";
11
17
  import { planLoopCommand } from "./plan-loop.js";
12
18
  import { presentCommand } from "./present.js";
19
+ import { presentOrchestratorCommand } from "./present-orchestrator.js";
13
20
  import { runCommand } from "./run.js";
14
21
  import { skillInstallCommand } from "./skill-install.js";
15
22
  import { specifyAreaCommand } from "./specify-area.js";
16
23
  import { styleCheckCommand } from "./style-check.js";
17
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 };
18
34
  export function registerCodebaseToSpec(program) {
19
35
  const cts = program
20
36
  .command("codebase-to-spec")
21
37
  .alias("cts")
22
38
  .description("Generate dotrequirements behavioral specifications from a codebase");
39
+ // Install SIGTERM/SIGINT handlers for every cts subcommand so an interrupted
40
+ // pipeline emits a structured `pipeline/aborted` line before exiting
41
+ // (CTS-OBSERVE-1.5). preAction fires after argv parsing and before the
42
+ // subcommand action runs.
43
+ cts.hook("preAction", () => {
44
+ installCtsAbortHandlers();
45
+ });
23
46
  cts
24
47
  .command("pack")
25
48
  .description("Pack the codebase (compressed and uncompressed views) into the cache")
26
49
  .option("-s, --scope <path>", "Limit to files under this path (subdir of project root)")
27
50
  .option("--fresh", "Clear the cache before running")
28
51
  .option("--budget <tokens>", "Override the working-context budget (in tokens)", (v) => parseInt(v, 10))
52
+ .option("--ignore-requirements", "Exclude `.requirements/**` from the pack (use when testing cts against a codebase whose existing requirements should not influence the output)")
29
53
  .action((opts) => packCommand({ ...opts, budgetTokens: opts.budget }));
30
54
  cts
31
55
  .command("plan-loop")
@@ -81,6 +105,7 @@ export function registerCodebaseToSpec(program) {
81
105
  .option("--non-interactive", "Force non-interactive mode (override TTY detection)")
82
106
  .option("--overwrite", "Replace existing .requirements/ files without prompting")
83
107
  .option("--skip-existing", "Leave existing .requirements/ files untouched")
108
+ .option("--ignore-requirements", "Write output to `.requirements/cts/` instead of `.requirements/` (use together with `cts pack --ignore-requirements` when isolating cts from an existing requirements directory)")
84
109
  .action((opts) => {
85
110
  const forceMode = opts.interactive
86
111
  ? "interactive"
@@ -91,6 +116,7 @@ export function registerCodebaseToSpec(program) {
91
116
  forceMode,
92
117
  overwrite: opts.overwrite,
93
118
  skipExisting: opts.skipExisting,
119
+ ignoreRequirements: opts.ignoreRequirements,
94
120
  });
95
121
  });
96
122
  cts
@@ -105,6 +131,7 @@ export function registerCodebaseToSpec(program) {
105
131
  .option("--non-interactive", "Force non-interactive mode for the present stage")
106
132
  .option("--overwrite", "Replace existing .requirements/ files without prompting")
107
133
  .option("--skip-existing", "Leave existing .requirements/ files untouched")
134
+ .option("--ignore-requirements", "Exclude `.requirements/**` from the pack and write output to `.requirements/cts/` instead of `.requirements/` (use when testing cts against a codebase whose existing requirements should not influence the output)")
108
135
  .action((opts) => {
109
136
  const forceMode = opts.interactive
110
137
  ? "interactive"
@@ -118,10 +145,51 @@ export function registerCodebaseToSpec(program) {
118
145
  forceMode,
119
146
  overwrite: opts.overwrite,
120
147
  skipExisting: opts.skipExisting,
148
+ ignoreRequirements: opts.ignoreRequirements,
149
+ });
150
+ });
151
+ cts
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,
121
189
  });
122
190
  });
123
191
  cts
124
- .command("skill-install")
192
+ .command("skill-install", HIDDEN)
125
193
  .description("Install the codebase-to-spec skill (SKILL.md) into the host's skills directory")
126
194
  .option("--global", "Install to ~/.claude/skills/ instead of the project")
127
195
  .option("--target-dir <path>", "Install into a custom skills directory (for non-Claude-Code hosts)")
@@ -13,6 +13,12 @@ export interface PackOptions {
13
13
  scope?: string;
14
14
  fresh?: boolean;
15
15
  budgetTokens?: number;
16
+ /**
17
+ * When true, exclude `.requirements/**` from the pack so an existing
18
+ * requirements directory does not influence the generated output.
19
+ * See CTS-PRESENT-5.
20
+ */
21
+ ignoreRequirements?: boolean;
16
22
  /** Override progress emitter, mainly for tests. */
17
23
  progress?: import("../../codebase-to-spec/progress.js").ProgressEmitter;
18
24
  /** Override the project root, mainly for tests. */
@@ -43,6 +43,7 @@ export async function packCommand(rawOptions = {}) {
43
43
  projectRoot,
44
44
  paths,
45
45
  scope: rawOptions.scope,
46
+ ignoreRequirements: rawOptions.ignoreRequirements,
46
47
  });
47
48
  progress.emit({
48
49
  stage: "pack",
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec present-orchestrator` subcommand.
3
+ *
4
+ * Conversational-orchestrator variant of `present`. Reads outline.yaml +
5
+ * composedSpec (produced by compose-orchestrator), writes the final spec
6
+ * to `.requirements/`. The orchestrator doesn't have a separate
7
+ * document-level edit-loop, so this reads from composedSpec directly
8
+ * rather than specFinal.
9
+ *
10
+ * Requirements covered:
11
+ * - CTS-PRESENT-1..4 (reuses the legacy present logic)
12
+ * - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
13
+ */
14
+ export interface PresentOrchestratorOptions {
15
+ forceMode?: "interactive" | "non-interactive";
16
+ overwrite?: boolean;
17
+ skipExisting?: boolean;
18
+ }
19
+ export declare function presentOrchestratorCommand(options?: PresentOrchestratorOptions): Promise<void>;
20
+ //# sourceMappingURL=present-orchestrator.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `dotrequirements codebase-to-spec present-orchestrator` subcommand.
3
+ *
4
+ * Conversational-orchestrator variant of `present`. Reads outline.yaml +
5
+ * composedSpec (produced by compose-orchestrator), writes the final spec
6
+ * to `.requirements/`. The orchestrator doesn't have a separate
7
+ * document-level edit-loop, so this reads from composedSpec directly
8
+ * rather than specFinal.
9
+ *
10
+ * Requirements covered:
11
+ * - CTS-PRESENT-1..4 (reuses the legacy present logic)
12
+ * - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
13
+ */
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { cachePaths } from "../../codebase-to-spec/cache.js";
16
+ import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
17
+ import { detectMode } from "../../codebase-to-spec/interactive.js";
18
+ import { runPresent, } from "../../codebase-to-spec/present.js";
19
+ import { conversationalOutlineToLegacy, parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
20
+ import { findProjectRoot } from "../../utils/project-settings.js";
21
+ function resolvePolicy(isInteractive, options) {
22
+ if (options.overwrite && options.skipExisting) {
23
+ return { error: "--overwrite and --skip-existing are mutually exclusive." };
24
+ }
25
+ if (options.overwrite)
26
+ return "overwrite";
27
+ if (options.skipExisting)
28
+ return "skip-existing";
29
+ if (isInteractive)
30
+ return "prompt";
31
+ return "fail-fast";
32
+ }
33
+ export async function presentOrchestratorCommand(options = {}) {
34
+ const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
35
+ const paths = cachePaths(projectRoot);
36
+ if (!existsSync(paths.outline)) {
37
+ process.stderr.write(`No outline.yaml found at ${paths.outline}. Run the orchestrator's planner first.\n`);
38
+ process.exitCode = ExitCode.MissingInput;
39
+ return;
40
+ }
41
+ if (!existsSync(paths.composedSpec)) {
42
+ process.stderr.write(`No composed spec at ${paths.composedSpec}. Run \`cts compose-orchestrator\` first.\n`);
43
+ process.exitCode = ExitCode.MissingInput;
44
+ return;
45
+ }
46
+ let outline;
47
+ try {
48
+ outline = parseConversationalOutline(readFileSync(paths.outline, "utf-8"));
49
+ }
50
+ catch (err) {
51
+ process.stderr.write(`Outline at ${paths.outline} is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
52
+ process.exitCode = ExitCode.MissingInput;
53
+ return;
54
+ }
55
+ const { isInteractive } = detectMode(options.forceMode);
56
+ const policy = resolvePolicy(isInteractive, options);
57
+ if (typeof policy === "object") {
58
+ process.stderr.write(`${policy.error}\n`);
59
+ process.exitCode = ExitCode.InvalidFlags;
60
+ return;
61
+ }
62
+ const legacyOutline = conversationalOutlineToLegacy(outline);
63
+ const result = await runPresent({
64
+ outline: legacyOutline,
65
+ finalSpecPath: paths.composedSpec,
66
+ projectRoot,
67
+ overwritePolicy: policy,
68
+ });
69
+ if (!result.allWritten) {
70
+ if (result.conflictPath) {
71
+ process.stderr.write(`Refusing to overwrite ${result.conflictPath}. Pass --overwrite or --skip-existing.\n`);
72
+ }
73
+ process.exitCode = ExitCode.OverwriteRefused;
74
+ return;
75
+ }
76
+ for (const action of result.actions) {
77
+ process.stdout.write(` ${action.action}: ${action.path}\n`);
78
+ }
79
+ process.stdout.write(`Wrote ${result.actions.filter((a) => a.action === "created" || a.action === "overwrote").length} file(s) to .requirements/\n`);
80
+ }
81
+ //# sourceMappingURL=present-orchestrator.js.map
@@ -15,6 +15,11 @@ export interface PresentCmdOptions {
15
15
  overwrite?: boolean;
16
16
  /** Skip writing files that would conflict (non-interactive). */
17
17
  skipExisting?: boolean;
18
+ /**
19
+ * When true, write output under `.requirements/cts/` instead of
20
+ * `.requirements/`. See CTS-PRESENT-5.
21
+ */
22
+ ignoreRequirements?: boolean;
18
23
  projectRoot?: string;
19
24
  }
20
25
  export declare function presentCommand(rawOptions?: PresentCmdOptions): Promise<void>;
@@ -51,16 +51,21 @@ export async function presentCommand(rawOptions = {}) {
51
51
  process.exitCode = ExitCode.InvalidFlags;
52
52
  return;
53
53
  }
54
+ const outputSubdir = rawOptions.ignoreRequirements ? "cts" : undefined;
55
+ const outputDirLabel = outputSubdir
56
+ ? `.requirements/${outputSubdir}/`
57
+ : ".requirements/";
54
58
  progress.emit({
55
59
  stage: "present",
56
60
  step: "start",
57
- message: `Writing final spec to .requirements/ (mode: ${isInteractive ? "interactive" : "non-interactive"}, policy: ${policy})`,
61
+ message: `Writing final spec to ${outputDirLabel} (mode: ${isInteractive ? "interactive" : "non-interactive"}, policy: ${policy})`,
58
62
  });
59
63
  const result = await runPresent({
60
64
  outline,
61
65
  finalSpecPath: paths.specFinal,
62
66
  projectRoot,
63
67
  overwritePolicy: policy,
68
+ outputSubdir,
64
69
  });
65
70
  // Per-file outcome announcements.
66
71
  for (const action of result.actions) {
@@ -77,6 +77,7 @@ export async function runCommand(options = {}) {
77
77
  forceMode: options.forceMode,
78
78
  overwrite: options.overwrite,
79
79
  skipExisting: options.skipExisting,
80
+ ignoreRequirements: options.ignoreRequirements,
80
81
  projectRoot: options.projectRoot,
81
82
  });
82
83
  // Present sets a non-zero exit code on overwrite refusal or validation
@@ -41,7 +41,18 @@ export async function skillInstallCommand(options = {}) {
41
41
  else {
42
42
  process.stdout.write(`SKILL.md already up to date at ${result.installedPath}\n`);
43
43
  }
44
- process.stdout.write("Skill ready. In Claude Code, invoke it with `/codebase-to-spec` (or via natural language).\n");
44
+ if (result.companions) {
45
+ process.stdout.write(`Installed cts-worker agent at ${result.companions.agentPath}\n`);
46
+ process.stdout.write(`Installed persona-injection hook at ${result.companions.hookScriptPath}\n`);
47
+ if (result.companions.hookRegistered) {
48
+ process.stdout.write(`Registered PreToolUse hook in ${result.companions.settingsPath}\n`);
49
+ }
50
+ else {
51
+ process.stdout.write(`PreToolUse hook already registered in ${result.companions.settingsPath}\n`);
52
+ }
53
+ process.stdout.write("\n⚠️ The hook script uses `dotrequirements` from PATH. If you're working in a local dev clone, set DOTREQUIREMENTS_CLI to override (see the hook script for details).\n");
54
+ }
55
+ process.stdout.write("\nSkill ready. In Claude Code, invoke it with `/codebase-to-spec` (or via natural language).\n");
45
56
  }
46
57
  catch (err) {
47
58
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: cts-worker
3
+ description: Generic worker subagent for the codebase-to-spec conversational orchestrator. Used for planner, specifier, editor, and reviewer dispatches. The per-dispatch persona is injected by the cts-worker-persona PreToolUse hook at dispatch time.
4
+ tools: Bash, Read, Edit, Write, Glob, Grep
5
+ ---
6
+
7
+ You are a codebase-to-spec worker. Your dispatched prompt is composed externally and supplied to you at dispatch time. It will specify your role for this dispatch (planner, specifier, editor, or reviewer), the artifacts you're working with, and the output convention you must follow.
8
+
9
+ Follow the dispatched prompt exactly. Do not improvise behavior beyond what it asks. When your work is done, submit via the convention specified in the dispatch (typically a `dotrequirements cts submit` call).
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook for Task(subagent_type=cts-worker) dispatches.
3
+ # Composes the worker's first-turn prompt by calling
4
+ # `dotrequirements cts dispatch-context <dispatch-id>` and emits the
5
+ # result via `updatedInput.prompt`.
6
+ #
7
+ # The orchestrator's convention: the original Task prompt MUST include
8
+ # the verbatim token `dispatch-id=<id>` so the hook can extract it.
9
+ #
10
+ # Bundled with the codebase-to-spec skill (CTSO-CLI-1).
11
+ #
12
+ # CLI resolution order:
13
+ # 1. DOTREQUIREMENTS_CLI env var (explicit override)
14
+ # 2. If running inside the dotrequirements monorepo itself
15
+ # ($CLAUDE_PROJECT_DIR/packages/cli/dist/cli.js exists), use that
16
+ # local build. Lets dotrequirements developers use the orchestrator
17
+ # against any worktree without a global install.
18
+ # 3. `dotrequirements` from PATH (the canonical end-user setup).
19
+
20
+ INPUT=$(cat)
21
+ SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // ""')
22
+
23
+ # Self-filter: only act on cts-worker dispatches.
24
+ if [[ "$SUBAGENT_TYPE" != "cts-worker" ]]; then
25
+ exit 0
26
+ fi
27
+
28
+ ORIGINAL_PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // ""')
29
+
30
+ # Extract dispatch-id token from the original prompt.
31
+ DISPATCH_ID=$(echo "$ORIGINAL_PROMPT" | grep -oE 'dispatch-id=[a-zA-Z0-9_-]+' | head -1 | sed 's/dispatch-id=//')
32
+
33
+ if [[ -z "$DISPATCH_ID" ]]; then
34
+ # No dispatch-id in the prompt — let it through unmodified.
35
+ exit 0
36
+ fi
37
+
38
+ # Resolve the CLI invocation (see header for order).
39
+ if [[ -n "${DOTREQUIREMENTS_CLI:-}" ]]; then
40
+ CLI="$DOTREQUIREMENTS_CLI"
41
+ elif [[ -n "${CLAUDE_PROJECT_DIR:-}" && -f "$CLAUDE_PROJECT_DIR/packages/cli/dist/cli.js" ]]; then
42
+ CLI="node $CLAUDE_PROJECT_DIR/packages/cli/dist/cli.js"
43
+ else
44
+ CLI="dotrequirements"
45
+ fi
46
+
47
+ # Compose the dispatch context. CLI returns JSON like {"prompt": "..."}.
48
+ COMPOSED=$($CLI cts dispatch-context "$DISPATCH_ID" 2>/dev/null)
49
+ COMPOSE_STATUS=$?
50
+
51
+ if [[ $COMPOSE_STATUS -ne 0 || -z "$COMPOSED" ]]; then
52
+ # CLI call failed — let the dispatch through unmodified so the
53
+ # worker's failure is legible.
54
+ exit 0
55
+ fi
56
+
57
+ PROMPT=$(echo "$COMPOSED" | jq -r '.prompt // ""')
58
+
59
+ if [[ -z "$PROMPT" ]]; then
60
+ exit 0
61
+ fi
62
+
63
+ # Emit updatedInput so the worker subagent sees the composed prompt
64
+ # as its first-turn input. (Field name is `updatedInput`, not
65
+ # `modifiedInput` — verified by spike work; see working doc.)
66
+ jq -nc --arg p "$PROMPT" '{
67
+ hookSpecificOutput: {
68
+ hookEventName: "PreToolUse",
69
+ permissionDecision: "allow",
70
+ updatedInput: {
71
+ prompt: $p
72
+ }
73
+ }
74
+ }'
75
+
76
+ exit 0