@popoverai/dotrequirements 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/README.md +167 -20
  2. package/dist/cli.js +121 -60
  3. package/dist/codebase-to-spec/budget.d.ts +53 -0
  4. package/dist/codebase-to-spec/budget.js +80 -0
  5. package/dist/codebase-to-spec/cache.d.ts +49 -0
  6. package/dist/codebase-to-spec/cache.js +54 -0
  7. package/dist/codebase-to-spec/claude.d.ts +69 -0
  8. package/dist/codebase-to-spec/claude.js +126 -0
  9. package/dist/codebase-to-spec/compose.d.ts +49 -0
  10. package/dist/codebase-to-spec/compose.js +124 -0
  11. package/dist/codebase-to-spec/edit-loop.d.ts +54 -0
  12. package/dist/codebase-to-spec/edit-loop.js +195 -0
  13. package/dist/codebase-to-spec/editor.d.ts +54 -0
  14. package/dist/codebase-to-spec/editor.js +74 -0
  15. package/dist/codebase-to-spec/exit-codes.d.ts +40 -0
  16. package/dist/codebase-to-spec/exit-codes.js +58 -0
  17. package/dist/codebase-to-spec/fan-out.d.ts +63 -0
  18. package/dist/codebase-to-spec/fan-out.js +215 -0
  19. package/dist/codebase-to-spec/interactive.d.ts +30 -0
  20. package/dist/codebase-to-spec/interactive.js +48 -0
  21. package/dist/codebase-to-spec/outline-review-loop.d.ts +51 -0
  22. package/dist/codebase-to-spec/outline-review-loop.js +187 -0
  23. package/dist/codebase-to-spec/pack.d.ts +51 -0
  24. package/dist/codebase-to-spec/pack.js +127 -0
  25. package/dist/codebase-to-spec/planner.d.ts +41 -0
  26. package/dist/codebase-to-spec/planner.js +76 -0
  27. package/dist/codebase-to-spec/present.d.ts +94 -0
  28. package/dist/codebase-to-spec/present.js +288 -0
  29. package/dist/codebase-to-spec/progress.d.ts +33 -0
  30. package/dist/codebase-to-spec/progress.js +28 -0
  31. package/dist/codebase-to-spec/prompts/editor.d.ts +13 -0
  32. package/dist/codebase-to-spec/prompts/editor.js +57 -0
  33. package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +12 -0
  34. package/dist/codebase-to-spec/prompts/outline-reviewer.js +87 -0
  35. package/dist/codebase-to-spec/prompts/planner-apply.d.ts +12 -0
  36. package/dist/codebase-to-spec/prompts/planner-apply.js +32 -0
  37. package/dist/codebase-to-spec/prompts/planner-initial.d.ts +11 -0
  38. package/dist/codebase-to-spec/prompts/planner-initial.js +125 -0
  39. package/dist/codebase-to-spec/prompts/planner-revise.d.ts +14 -0
  40. package/dist/codebase-to-spec/prompts/planner-revise.js +60 -0
  41. package/dist/codebase-to-spec/prompts/spec-reviewer.d.ts +16 -0
  42. package/dist/codebase-to-spec/prompts/spec-reviewer.js +96 -0
  43. package/dist/codebase-to-spec/prompts/specifier.d.ts +12 -0
  44. package/dist/codebase-to-spec/prompts/specifier.js +100 -0
  45. package/dist/codebase-to-spec/prompts/style-check.d.ts +12 -0
  46. package/dist/codebase-to-spec/prompts/style-check.js +78 -0
  47. package/dist/codebase-to-spec/schemas.d.ts +257 -0
  48. package/dist/codebase-to-spec/schemas.js +183 -0
  49. package/dist/codebase-to-spec/skill-install.d.ts +57 -0
  50. package/dist/codebase-to-spec/skill-install.js +79 -0
  51. package/dist/codebase-to-spec/slice.d.ts +49 -0
  52. package/dist/codebase-to-spec/slice.js +111 -0
  53. package/dist/codebase-to-spec/specifier.d.ts +60 -0
  54. package/dist/codebase-to-spec/specifier.js +79 -0
  55. package/dist/codebase-to-spec/style-check.d.ts +29 -0
  56. package/dist/codebase-to-spec/style-check.js +33 -0
  57. package/dist/codebase-to-spec/summary.d.ts +51 -0
  58. package/dist/codebase-to-spec/summary.js +183 -0
  59. package/dist/codebase-to-spec/validate.d.ts +46 -0
  60. package/dist/codebase-to-spec/validate.js +130 -0
  61. package/dist/commands/acceptance-test.d.ts +6 -0
  62. package/dist/commands/acceptance-test.js +212 -0
  63. package/dist/commands/ai-setup.d.ts +5 -0
  64. package/dist/commands/ai-setup.js +441 -0
  65. package/dist/commands/browsertest.js +34 -27
  66. package/dist/commands/codebase-to-spec/compose.d.ts +14 -0
  67. package/dist/commands/codebase-to-spec/compose.js +57 -0
  68. package/dist/commands/codebase-to-spec/edit-loop.d.ts +16 -0
  69. package/dist/commands/codebase-to-spec/edit-loop.js +83 -0
  70. package/dist/commands/codebase-to-spec/fan-out.d.ts +19 -0
  71. package/dist/commands/codebase-to-spec/fan-out.js +77 -0
  72. package/dist/commands/codebase-to-spec/index.d.ts +9 -0
  73. package/dist/commands/codebase-to-spec/index.js +135 -0
  74. package/dist/commands/codebase-to-spec/pack.d.ts +22 -0
  75. package/dist/commands/codebase-to-spec/pack.js +76 -0
  76. package/dist/commands/codebase-to-spec/plan-loop.d.ts +26 -0
  77. package/dist/commands/codebase-to-spec/plan-loop.js +105 -0
  78. package/dist/commands/codebase-to-spec/present.d.ts +21 -0
  79. package/dist/commands/codebase-to-spec/present.js +92 -0
  80. package/dist/commands/codebase-to-spec/run.d.ts +20 -0
  81. package/dist/commands/codebase-to-spec/run.js +85 -0
  82. package/dist/commands/codebase-to-spec/skill-install.d.ts +20 -0
  83. package/dist/commands/codebase-to-spec/skill-install.js +51 -0
  84. package/dist/commands/codebase-to-spec/specify-area.d.ts +18 -0
  85. package/dist/commands/codebase-to-spec/specify-area.js +82 -0
  86. package/dist/commands/codebase-to-spec/style-check.d.ts +15 -0
  87. package/dist/commands/codebase-to-spec/style-check.js +42 -0
  88. package/dist/commands/codebase-to-spec/validate.d.ts +18 -0
  89. package/dist/commands/codebase-to-spec/validate.js +38 -0
  90. package/dist/commands/create-requirement-document.d.ts +2 -0
  91. package/dist/commands/create-requirement-document.js +41 -0
  92. package/dist/commands/finalize.js +7 -7
  93. package/dist/commands/get.d.ts +2 -0
  94. package/dist/commands/get.js +55 -0
  95. package/dist/commands/init.js +132 -117
  96. package/dist/commands/link.js +27 -27
  97. package/dist/commands/list.d.ts +6 -0
  98. package/dist/commands/list.js +43 -0
  99. package/dist/commands/mcp-setup.js +159 -149
  100. package/dist/commands/mcp.js +1 -1
  101. package/dist/commands/prepare.js +4 -4
  102. package/dist/commands/pull.js +116 -121
  103. package/dist/commands/push.js +106 -112
  104. package/dist/commands/report.d.ts +6 -2
  105. package/dist/commands/report.js +177 -122
  106. package/dist/commands/requirements-for.d.ts +2 -0
  107. package/dist/commands/requirements-for.js +29 -0
  108. package/dist/commands/review-test.d.ts +2 -0
  109. package/dist/commands/review-test.js +75 -0
  110. package/dist/commands/search.d.ts +6 -0
  111. package/dist/commands/search.js +39 -0
  112. package/dist/commands/style-check.d.ts +7 -0
  113. package/dist/commands/style-check.js +75 -0
  114. package/dist/commands/test.js +53 -59
  115. package/dist/commands/tests-for.d.ts +2 -0
  116. package/dist/commands/tests-for.js +80 -0
  117. package/dist/commands/validate.d.ts +6 -0
  118. package/dist/commands/validate.js +72 -0
  119. package/dist/config.js +1 -1
  120. package/dist/convex.d.ts +34 -22
  121. package/dist/convex.js +38 -22
  122. package/dist/harness/cache.d.ts +1 -5
  123. package/dist/harness/cache.js +49 -59
  124. package/dist/harness/convexReporting.d.ts +1 -1
  125. package/dist/harness/convexReporting.js +9 -7
  126. package/dist/harness/coverageCache.js +3 -3
  127. package/dist/harness/finalize.js +59 -46
  128. package/dist/harness/index.d.ts +6 -7
  129. package/dist/harness/index.js +9 -10
  130. package/dist/harness/prepare.js +6 -5
  131. package/dist/harness/requirementsLoader.d.ts +2 -2
  132. package/dist/harness/requirementsLoader.js +13 -35
  133. package/dist/harness/tracking.js +18 -18
  134. package/dist/harness/types.d.ts +1 -1
  135. package/dist/mcp/convexClient.d.ts +0 -39
  136. package/dist/mcp/convexClient.js +2 -107
  137. package/dist/mcp/grep.d.ts +1 -1
  138. package/dist/mcp/grep.js +87 -42
  139. package/dist/mcp/handlers/authoring.d.ts +1 -1
  140. package/dist/mcp/handlers/authoring.js +30 -234
  141. package/dist/mcp/handlers/coverage.d.ts +1 -1
  142. package/dist/mcp/handlers/coverage.js +13 -15
  143. package/dist/mcp/handlers/debug.d.ts +2 -3
  144. package/dist/mcp/handlers/debug.js +10 -10
  145. package/dist/mcp/handlers/get.d.ts +1 -1
  146. package/dist/mcp/handlers/get.js +11 -10
  147. package/dist/mcp/handlers/index.d.ts +20 -20
  148. package/dist/mcp/handlers/index.js +10 -10
  149. package/dist/mcp/handlers/list.d.ts +4 -33
  150. package/dist/mcp/handlers/list.js +16 -38
  151. package/dist/mcp/handlers/push.d.ts +1 -1
  152. package/dist/mcp/handlers/push.js +28 -18
  153. package/dist/mcp/handlers/report.d.ts +16 -0
  154. package/dist/mcp/handlers/report.js +134 -0
  155. package/dist/mcp/handlers/review.d.ts +1 -1
  156. package/dist/mcp/handlers/review.js +40 -59
  157. package/dist/mcp/handlers/search.d.ts +1 -1
  158. package/dist/mcp/handlers/search.js +7 -9
  159. package/dist/mcp/handlers/test-mapping.d.ts +1 -1
  160. package/dist/mcp/handlers/test-mapping.js +14 -14
  161. package/dist/mcp/handlers/types.d.ts +3 -3
  162. package/dist/mcp/handlers/types.js +2 -2
  163. package/dist/mcp/index.d.ts +1 -1
  164. package/dist/mcp/index.js +147 -167
  165. package/dist/mcp/requirements.d.ts +2 -2
  166. package/dist/mcp/requirements.js +30 -30
  167. package/dist/mcp/testCodeExtractor.js +24 -26
  168. package/dist/mcp/types.d.ts +1 -1
  169. package/dist/push/core.d.ts +2 -2
  170. package/dist/push/core.js +20 -20
  171. package/dist/push/index.d.ts +1 -1
  172. package/dist/push/index.js +2 -2
  173. package/dist/requirements/cloud-ai.d.ts +57 -0
  174. package/dist/requirements/cloud-ai.js +104 -0
  175. package/dist/requirements/cloud-coverage.d.ts +41 -0
  176. package/dist/requirements/cloud-coverage.js +60 -0
  177. package/dist/requirements/coverage.d.ts +45 -0
  178. package/dist/requirements/coverage.js +114 -0
  179. package/dist/requirements/grep.d.ts +33 -0
  180. package/dist/requirements/grep.js +306 -0
  181. package/dist/requirements/index.d.ts +73 -0
  182. package/dist/requirements/index.js +174 -0
  183. package/dist/requirements/style-guide.d.ts +67 -0
  184. package/dist/requirements/style-guide.js +299 -0
  185. package/dist/requirements/testCodeExtractor.d.ts +22 -0
  186. package/dist/requirements/testCodeExtractor.js +150 -0
  187. package/dist/schema/browser.d.ts +8 -8
  188. package/dist/schema/browser.js +13 -15
  189. package/dist/schema/builder.d.ts +1 -1
  190. package/dist/schema/builder.js +13 -44
  191. package/dist/schema/conversions.d.ts +2 -2
  192. package/dist/schema/conversions.js +11 -11
  193. package/dist/schema/index.d.ts +9 -9
  194. package/dist/schema/index.js +15 -15
  195. package/dist/schema/parser-core.d.ts +1 -1
  196. package/dist/schema/parser-core.js +23 -22
  197. package/dist/schema/parser.d.ts +3 -3
  198. package/dist/schema/parser.js +27 -31
  199. package/dist/schema/resolver.d.ts +1 -1
  200. package/dist/schema/resolver.js +9 -9
  201. package/dist/schema/scenario.d.ts +1 -1
  202. package/dist/schema/scenario.js +1 -1
  203. package/dist/schema/schemas.d.ts +3 -3
  204. package/dist/schema/schemas.js +41 -28
  205. package/dist/schema/test-schema.js +27 -27
  206. package/dist/templates/context-file-section.md +3 -2
  207. package/dist/templates/example-requirements.js +1 -1
  208. package/dist/templates/example-requirements.ts +3 -1
  209. package/dist/templates/requirements-readme.js +1 -1
  210. package/dist/templates/requirements-readme.ts +1 -1
  211. package/dist/templates/skills/codebase-to-spec/SKILL.md +118 -0
  212. package/dist/utils/brand.js +3 -3
  213. package/dist/utils/browser-launch.js +4 -4
  214. package/dist/utils/context-file.d.ts +1 -1
  215. package/dist/utils/context-file.js +26 -26
  216. package/dist/utils/env.js +7 -7
  217. package/dist/utils/gitignore.js +7 -7
  218. package/dist/utils/oauth-callback-server.d.ts +1 -1
  219. package/dist/utils/oauth-callback-server.js +27 -25
  220. package/dist/utils/oauth-flow.js +32 -29
  221. package/dist/utils/project-discovery.d.ts +3 -3
  222. package/dist/utils/project-discovery.js +18 -17
  223. package/dist/utils/project-name.js +8 -8
  224. package/dist/utils/project-selector.d.ts +1 -1
  225. package/dist/utils/project-selector.js +24 -21
  226. package/dist/utils/project-settings.d.ts +1 -1
  227. package/dist/utils/project-settings.js +24 -22
  228. package/dist/utils/templates.js +6 -6
  229. package/package.json +2 -1
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Cache directory layout and path helpers for codebase-to-spec.
3
+ *
4
+ * All pipeline artifacts live under `.dotrequirements-cache/` in the project
5
+ * root. Wrappers and downstream stages read cached inputs by these paths;
6
+ * resumability (CTS-RESUME-1..4) is driven by the presence/absence of these
7
+ * files.
8
+ *
9
+ * Requirements covered:
10
+ * - CTS-PACK-1: pack stage paths (overview.txt, source.txt)
11
+ * - CTS-RESUME-1..4: per-stage cache detection
12
+ */
13
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ export const CACHE_DIR_NAME = ".dotrequirements-cache";
16
+ export function cachePaths(projectRoot) {
17
+ const root = join(projectRoot, CACHE_DIR_NAME);
18
+ return {
19
+ root,
20
+ overview: join(root, "overview.txt"),
21
+ source: join(root, "source.txt"),
22
+ outlineFinal: join(root, "outline-final.json"),
23
+ outlineTurn: (n) => join(root, `outline-${n}.json`),
24
+ outlineReviewTurn: (n) => join(root, `outline-review-${n}.json`),
25
+ partialsDir: join(root, "partials"),
26
+ partial: (name) => join(root, "partials", `${name}.partial.md`),
27
+ composedSpec: join(root, "spec-composed.md"),
28
+ specVersion: (n) => join(root, `spec-v${n}.md`),
29
+ specReviewTurn: (n) => join(root, `spec-review-${n}.json`),
30
+ specFinal: join(root, "spec-final.md"),
31
+ pipelineSummary: join(root, "pipeline-summary.json"),
32
+ };
33
+ }
34
+ export function ensureCacheDir(projectRoot) {
35
+ const paths = cachePaths(projectRoot);
36
+ if (!existsSync(paths.root)) {
37
+ mkdirSync(paths.root, { recursive: true });
38
+ }
39
+ if (!existsSync(paths.partialsDir)) {
40
+ mkdirSync(paths.partialsDir, { recursive: true });
41
+ }
42
+ return paths;
43
+ }
44
+ /**
45
+ * Remove all cached artifacts. Used by `--fresh`.
46
+ * Requirement: CTS-RESUME-4
47
+ */
48
+ export function clearCache(projectRoot) {
49
+ const root = join(projectRoot, CACHE_DIR_NAME);
50
+ if (existsSync(root)) {
51
+ rmSync(root, { recursive: true, force: true });
52
+ }
53
+ }
54
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `claude -p` subprocess wrapper for codebase-to-spec.
3
+ *
4
+ * Provides a small typed surface for invoking the user's Claude Code agent in
5
+ * non-interactive (print) mode, with optional stateful session continuation
6
+ * and JSON-schema output validation.
7
+ *
8
+ * Why this lives here:
9
+ * - The pipeline spawns the user's claude binary, inheriting the user's
10
+ * environment so whatever auth mode they've configured (API key, OAuth via
11
+ * Claude Code subscription, etc.) is what gets used.
12
+ * - Each invocation is a separate context window. Stateful loops use
13
+ * --session-id and --resume to maintain conversation state across turns.
14
+ *
15
+ * Requirements covered:
16
+ * - CTS-PLAN-2, CTS-EDIT-1: stateful reviewer sessions via --session-id / --resume
17
+ * - CTS-PLAN-2, CTS-EDIT-1: JSON-schema-validated output via --json-schema
18
+ */
19
+ export interface ClaudeRunOptions {
20
+ /** System prompt content (passed via --system-prompt). */
21
+ systemPrompt: string;
22
+ /** User message (passed as final positional arg). */
23
+ userMessage: string;
24
+ /** Model alias (e.g., 'sonnet') or full name. Defaults to 'sonnet'. */
25
+ model?: string;
26
+ /** Tools the agent may use. Defaults to ['Read']. */
27
+ tools?: string[];
28
+ /** Directories to grant filesystem access to (via --add-dir). */
29
+ addDirs?: string[];
30
+ /** Optional JSON schema for structured output validation. */
31
+ jsonSchema?: object;
32
+ /**
33
+ * Stateful session control:
34
+ * - `{ kind: 'fresh', sessionId: '<uuid>' }` — create a new session with this id
35
+ * - `{ kind: 'resume', sessionId: '<uuid>' }` — continue an existing session
36
+ * - omitted / undefined — stateless, no persistence
37
+ */
38
+ session?: {
39
+ kind: "fresh";
40
+ sessionId: string;
41
+ } | {
42
+ kind: "resume";
43
+ sessionId: string;
44
+ };
45
+ /** Override the working directory. Defaults to system temp dir. */
46
+ cwd?: string;
47
+ /** Override permission mode. Defaults to 'bypassPermissions'. */
48
+ permissionMode?: "bypassPermissions" | "auto" | "default" | "acceptEdits";
49
+ /** Path to the claude binary. Defaults to 'claude' (found in PATH). */
50
+ claudeBinary?: string;
51
+ }
52
+ export interface ClaudeRunResult {
53
+ /** Process exit code. */
54
+ exitCode: number;
55
+ /** Captured stdout. */
56
+ stdout: string;
57
+ /** Captured stderr. */
58
+ stderr: string;
59
+ }
60
+ /**
61
+ * Default runner — spawns the `claude` binary as a subprocess.
62
+ */
63
+ export declare function runClaude(options: ClaudeRunOptions): Promise<ClaudeRunResult>;
64
+ /**
65
+ * Pluggable runner interface so tests can swap in a mock without spawning
66
+ * a real subprocess. Production code uses `runClaude`; tests pass a fake.
67
+ */
68
+ export type ClaudeRunner = (options: ClaudeRunOptions) => Promise<ClaudeRunResult>;
69
+ //# sourceMappingURL=claude.d.ts.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `claude -p` subprocess wrapper for codebase-to-spec.
3
+ *
4
+ * Provides a small typed surface for invoking the user's Claude Code agent in
5
+ * non-interactive (print) mode, with optional stateful session continuation
6
+ * and JSON-schema output validation.
7
+ *
8
+ * Why this lives here:
9
+ * - The pipeline spawns the user's claude binary, inheriting the user's
10
+ * environment so whatever auth mode they've configured (API key, OAuth via
11
+ * Claude Code subscription, etc.) is what gets used.
12
+ * - Each invocation is a separate context window. Stateful loops use
13
+ * --session-id and --resume to maintain conversation state across turns.
14
+ *
15
+ * Requirements covered:
16
+ * - CTS-PLAN-2, CTS-EDIT-1: stateful reviewer sessions via --session-id / --resume
17
+ * - CTS-PLAN-2, CTS-EDIT-1: JSON-schema-validated output via --json-schema
18
+ */
19
+ import { spawn } from "node:child_process";
20
+ import { tmpdir } from "node:os";
21
+ /**
22
+ * Default runner — spawns the `claude` binary as a subprocess.
23
+ */
24
+ export async function runClaude(options) {
25
+ const { systemPrompt, userMessage, model = "sonnet", tools = ["Read"], addDirs = [], jsonSchema, session, cwd = tmpdir(), permissionMode = "bypassPermissions", claudeBinary = "claude", } = options;
26
+ const args = ["-p"];
27
+ args.push("--model", model);
28
+ args.push("--permission-mode", permissionMode);
29
+ // `--output-format text` does NOT enforce `--json-schema` (we verified this
30
+ // empirically). When we want schema-enforced JSON we need
31
+ // `--output-format json`, which wraps the response in a metadata envelope
32
+ // whose `structured_output` field contains the validated JSON.
33
+ args.push("--output-format", jsonSchema ? "json" : "text");
34
+ if (tools.length > 0) {
35
+ args.push("--tools", tools.join(","));
36
+ }
37
+ for (const dir of addDirs) {
38
+ args.push("--add-dir", dir);
39
+ }
40
+ if (jsonSchema) {
41
+ args.push("--json-schema", JSON.stringify(jsonSchema));
42
+ }
43
+ if (session) {
44
+ if (session.kind === "fresh") {
45
+ args.push("--session-id", session.sessionId);
46
+ }
47
+ else {
48
+ args.push("--resume", session.sessionId);
49
+ // On resume we still need to re-pass permission mode and add-dirs;
50
+ // resume doesn't preserve them by default.
51
+ }
52
+ }
53
+ else {
54
+ args.push("--no-session-persistence");
55
+ }
56
+ // System prompt — only on initial calls. On resume, the system prompt is
57
+ // already baked into the session and re-passing it can confuse the agent.
58
+ if (!session || session.kind === "fresh") {
59
+ args.push("--append-system-prompt", systemPrompt);
60
+ }
61
+ // The user message is always the final positional arg.
62
+ args.push(userMessage);
63
+ return new Promise((resolve, reject) => {
64
+ const child = spawn(claudeBinary, args, {
65
+ cwd,
66
+ // Inherit the parent environment so `claude -p` uses whatever auth
67
+ // mode the user has configured (API key if set, OAuth otherwise).
68
+ stdio: ["pipe", "pipe", "pipe"],
69
+ });
70
+ // Close stdin immediately — we pass user message via argv, not stdin.
71
+ child.stdin.end();
72
+ let stdout = "";
73
+ let stderr = "";
74
+ child.stdout.on("data", (chunk) => {
75
+ stdout += chunk.toString();
76
+ });
77
+ child.stderr.on("data", (chunk) => {
78
+ stderr += chunk.toString();
79
+ });
80
+ child.on("error", reject);
81
+ child.on("close", (code) => {
82
+ const exitCode = code ?? 0;
83
+ // If we asked for schema-validated JSON, claude -p returns a metadata
84
+ // envelope. Extract the agent's actual output from `structured_output`
85
+ // (preferred) or `result` (fallback). Callers expect `stdout` to
86
+ // contain only the agent's output, not the envelope.
87
+ if (jsonSchema && exitCode === 0 && stdout.trim().length > 0) {
88
+ try {
89
+ const envelope = JSON.parse(stdout);
90
+ if (envelope && typeof envelope === "object") {
91
+ if (envelope.is_error) {
92
+ // Surface the API error message on stderr so callers can see it.
93
+ const apiErrSnippet = typeof envelope.result === "string"
94
+ ? envelope.result
95
+ : JSON.stringify(envelope);
96
+ resolve({
97
+ exitCode: exitCode || 1,
98
+ stdout: "",
99
+ stderr: `${stderr}\nclaude -p reported is_error: ${apiErrSnippet}`,
100
+ });
101
+ return;
102
+ }
103
+ if ("structured_output" in envelope) {
104
+ resolve({
105
+ exitCode,
106
+ stdout: JSON.stringify(envelope.structured_output),
107
+ stderr,
108
+ });
109
+ return;
110
+ }
111
+ if (typeof envelope.result === "string") {
112
+ resolve({ exitCode, stdout: envelope.result, stderr });
113
+ return;
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ // Fall through to raw stdout. Caller's parser will surface the
119
+ // schema-validation failure.
120
+ }
121
+ }
122
+ resolve({ exitCode, stdout, stderr });
123
+ });
124
+ });
125
+ }
126
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Compose stage: assemble per-area partials into a single composed spec.
3
+ *
4
+ * The composed document has YAML frontmatter (title, defaultPrefix), an H1
5
+ * title, a summary paragraph from the outline, and one H2 section per area.
6
+ * Each area section contains the partial's content.
7
+ *
8
+ * After assembly, schema validation runs over the document. Mechanical errors
9
+ * (duplicate IDs within an area, etc.) trigger deterministic fixes; other
10
+ * errors surface to the caller.
11
+ *
12
+ * Requirements covered:
13
+ * - CTS-COMPOSE-1: Compose produces a single Markdown document from partials
14
+ * - CTS-COMPOSE-2: Compose validates the resulting document and corrects mechanical errors
15
+ */
16
+ import type { Outline } from "./schemas.js";
17
+ export interface ComposeOptions {
18
+ outline: Outline;
19
+ /** Resolves an area's sanitized name to its partial file path. */
20
+ partialPathFor: (sanitizedAreaName: string) => string;
21
+ /** Where to write the composed spec. */
22
+ composedPath: string;
23
+ }
24
+ export interface ComposeResult {
25
+ /** Path the composed spec was written to. */
26
+ composedPath: string;
27
+ /** Number of partials that contributed content. */
28
+ partialsIncluded: number;
29
+ /** Number of areas whose partial was missing or marked as missing. */
30
+ partialsMissing: number;
31
+ /** Validation outcome — undefined when validation passes. */
32
+ validationError?: string;
33
+ }
34
+ /**
35
+ * Assemble the composed Markdown document.
36
+ *
37
+ * Public so tests can call without disk side effects (composedPath isn't used
38
+ * here; the caller writes the returned string).
39
+ */
40
+ export declare function assembleComposedDocument(outline: Outline, partialPathFor: (sanitized: string) => string): {
41
+ content: string;
42
+ partialsIncluded: number;
43
+ partialsMissing: number;
44
+ };
45
+ /**
46
+ * Compose the spec and write it to disk. Validates the result.
47
+ */
48
+ export declare function runCompose(options: ComposeOptions): ComposeResult;
49
+ //# sourceMappingURL=compose.d.ts.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Compose stage: assemble per-area partials into a single composed spec.
3
+ *
4
+ * The composed document has YAML frontmatter (title, defaultPrefix), an H1
5
+ * title, a summary paragraph from the outline, and one H2 section per area.
6
+ * Each area section contains the partial's content.
7
+ *
8
+ * After assembly, schema validation runs over the document. Mechanical errors
9
+ * (duplicate IDs within an area, etc.) trigger deterministic fixes; other
10
+ * errors surface to the caller.
11
+ *
12
+ * Requirements covered:
13
+ * - CTS-COMPOSE-1: Compose produces a single Markdown document from partials
14
+ * - CTS-COMPOSE-2: Compose validates the resulting document and corrects mechanical errors
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { parseRequirementsFile } from "../schema/parser.js";
18
+ import { sanitizeAreaName } from "./fan-out.js";
19
+ /**
20
+ * Read a partial file. If missing or marked as a missing-partial placeholder,
21
+ * return null (caller will emit a stub section).
22
+ */
23
+ function readPartial(path) {
24
+ if (!existsSync(path))
25
+ return null;
26
+ const content = readFileSync(path, "utf-8");
27
+ const trimmed = content.trim();
28
+ if (!trimmed)
29
+ return null;
30
+ // Common placeholders for missing partials produced by fan-out on failure
31
+ // or by zero-files-assigned areas.
32
+ if (trimmed.startsWith("_(missing partial") ||
33
+ trimmed.startsWith("_(no files assigned")) {
34
+ return null;
35
+ }
36
+ return content;
37
+ }
38
+ /**
39
+ * Assemble the composed Markdown document.
40
+ *
41
+ * Public so tests can call without disk side effects (composedPath isn't used
42
+ * here; the caller writes the returned string).
43
+ */
44
+ export function assembleComposedDocument(outline, partialPathFor) {
45
+ const parts = [];
46
+ parts.push("---");
47
+ parts.push("document:");
48
+ parts.push(` title: "${escapeYamlString(outline.title)}"`);
49
+ parts.push(` defaultPrefix: ${outline.defaultPrefix}`);
50
+ parts.push("---");
51
+ parts.push("");
52
+ parts.push(`# ${outline.title}`);
53
+ parts.push("");
54
+ parts.push(outline.summary.trim());
55
+ parts.push("");
56
+ let partialsIncluded = 0;
57
+ let partialsMissing = 0;
58
+ for (const area of outline.areas) {
59
+ const sanitized = sanitizeAreaName(area.name);
60
+ const partialPath = partialPathFor(sanitized);
61
+ parts.push("---");
62
+ parts.push("");
63
+ parts.push(`## ${area.name}`);
64
+ parts.push("");
65
+ const partial = readPartial(partialPath);
66
+ if (partial) {
67
+ parts.push(partial.trim());
68
+ partialsIncluded++;
69
+ }
70
+ else {
71
+ parts.push(`_(partial for ${area.name} is missing or empty — surface this to the user when presenting the spec)_`);
72
+ partialsMissing++;
73
+ }
74
+ parts.push("");
75
+ }
76
+ return {
77
+ content: parts.join("\n"),
78
+ partialsIncluded,
79
+ partialsMissing,
80
+ };
81
+ }
82
+ /** Escape a string for safe YAML scalar use. */
83
+ function escapeYamlString(s) {
84
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
85
+ }
86
+ /**
87
+ * Attempt to fix mechanical errors in a composed document.
88
+ *
89
+ * For v0, the simplest mechanical fix is duplicate IDs within an area —
90
+ * we detect and renumber them sequentially. Position-path and Gherkin syntax
91
+ * errors surface to the validator's caller; we don't auto-fix those because
92
+ * the editor loop is better suited to it.
93
+ */
94
+ function fixMechanicalErrors(content) {
95
+ // Detect duplicate IDs (lines like "PREFIX-AREA-001: ...") within each area.
96
+ // For v0 we punt — the schema validator will catch them and the caller will
97
+ // surface to the user. Returning content unchanged keeps the contract: the
98
+ // function is a no-op for inputs without fixable errors.
99
+ return content;
100
+ }
101
+ /**
102
+ * Compose the spec and write it to disk. Validates the result.
103
+ */
104
+ export function runCompose(options) {
105
+ const { outline, partialPathFor, composedPath } = options;
106
+ const { content, partialsIncluded, partialsMissing } = assembleComposedDocument(outline, partialPathFor);
107
+ const finalContent = fixMechanicalErrors(content);
108
+ writeFileSync(composedPath, finalContent, "utf-8");
109
+ // Validate
110
+ let validationError;
111
+ try {
112
+ parseRequirementsFile(finalContent);
113
+ }
114
+ catch (err) {
115
+ validationError = err instanceof Error ? err.message : String(err);
116
+ }
117
+ return {
118
+ composedPath,
119
+ partialsIncluded,
120
+ partialsMissing,
121
+ validationError,
122
+ };
123
+ }
124
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Spec review + editor loop.
3
+ *
4
+ * Same structure as the outline review loop, scaled up for document-level
5
+ * revision: stateful spec reviewer + stateless editor. The reviewer
6
+ * persists across turns so it can compare prior spec versions to the latest.
7
+ *
8
+ * Requirements covered:
9
+ * - CTS-EDIT-1: stateful spec reviewer
10
+ * - CTS-EDIT-2: approved → proceed
11
+ * - CTS-EDIT-3: approved-with-revisions → one mechanical edit pass
12
+ * - CTS-EDIT-4: requires-another-review → revise & resubmit
13
+ * - CTS-EDIT-5: editor operates on cohesive document
14
+ * - CTS-EDIT-6: max-turns + convergence nudge
15
+ */
16
+ import { type ClaudeRunner } from "./claude.js";
17
+ import type { ProgressEmitter } from "./progress.js";
18
+ import { type SpecReview } from "./schemas.js";
19
+ export declare const DEFAULT_EDIT_LOOP_MAX_TURNS = 6;
20
+ export interface EditLoopOptions {
21
+ /** Path to the composed spec (input). */
22
+ composedSpecPath: string;
23
+ /**
24
+ * Resolver: given a turn number (1-indexed), return the path the editor's
25
+ * output for that turn should land at. Loop persists each version.
26
+ */
27
+ specVersionFor: (turn: number) => string;
28
+ /** Path to the codebase pack. */
29
+ fullPackPath: string;
30
+ /** Directories for filesystem access. */
31
+ addDirs: string[];
32
+ maxTurns?: number;
33
+ runner?: ClaudeRunner;
34
+ model?: string;
35
+ progress?: ProgressEmitter;
36
+ /** Override session id (for tests / reproducibility). */
37
+ sessionId?: string;
38
+ }
39
+ export interface EditLoopResult {
40
+ /** Path to the final spec (post-edit if applicable). */
41
+ finalSpecPath: string;
42
+ /** Per-turn spec paths (turn 1 is the composed-spec copy, then each revision). */
43
+ specPathsByTurn: string[];
44
+ /** Per-turn reviewer results. */
45
+ reviewsByTurn: SpecReview[];
46
+ /** The final reviewer verdict. */
47
+ finalVerdict: SpecReview["verdict"];
48
+ /** Number of review turns used. */
49
+ turnsUsed: number;
50
+ /** True if the loop hit max-turns without converging. */
51
+ hitMaxTurns: boolean;
52
+ }
53
+ export declare function runEditLoop(options: EditLoopOptions): Promise<EditLoopResult>;
54
+ //# sourceMappingURL=edit-loop.d.ts.map
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Spec review + editor loop.
3
+ *
4
+ * Same structure as the outline review loop, scaled up for document-level
5
+ * revision: stateful spec reviewer + stateless editor. The reviewer
6
+ * persists across turns so it can compare prior spec versions to the latest.
7
+ *
8
+ * Requirements covered:
9
+ * - CTS-EDIT-1: stateful spec reviewer
10
+ * - CTS-EDIT-2: approved → proceed
11
+ * - CTS-EDIT-3: approved-with-revisions → one mechanical edit pass
12
+ * - CTS-EDIT-4: requires-another-review → revise & resubmit
13
+ * - CTS-EDIT-5: editor operates on cohesive document
14
+ * - CTS-EDIT-6: max-turns + convergence nudge
15
+ */
16
+ import { randomUUID } from "node:crypto";
17
+ import { copyFileSync } from "node:fs";
18
+ import { runClaude } from "./claude.js";
19
+ import { runEditor } from "./editor.js";
20
+ import { SPEC_REVIEWER_PROMPT } from "./prompts/spec-reviewer.js";
21
+ import { parseSpecReview, SPEC_REVIEW_JSON_SCHEMA, } from "./schemas.js";
22
+ export const DEFAULT_EDIT_LOOP_MAX_TURNS = 6;
23
+ function reviewerUserMessageInitial(specPath, packPath) {
24
+ return [
25
+ `Initial spec review.`,
26
+ ``,
27
+ `Composed spec: ${specPath}`,
28
+ `Codebase pack: ${packPath}`,
29
+ ``,
30
+ `Read both via the Read tool, then produce your critique JSON.`,
31
+ ].join("\n");
32
+ }
33
+ function reviewerUserMessageRevised(turn, specPath, packPath, nudge) {
34
+ const lines = [
35
+ `Review turn ${turn}. The editor has revised the spec based on your prior feedback.`,
36
+ ``,
37
+ ];
38
+ if (nudge) {
39
+ lines.push(nudge, ``);
40
+ }
41
+ lines.push(`Revised spec: ${specPath}`, `Codebase pack at ${packPath} is unchanged.`, ``, `Read the revised spec, compare to the prior version, and produce your critique JSON.`);
42
+ return lines.join("\n");
43
+ }
44
+ function convergenceNudge(turn, maxTurns) {
45
+ return `CONVERGENCE NUDGE: This is review turn ${turn} of ${maxTurns} maximum. Please prioritize convergence. Your job is still to only approve a genuinely-ready spec, but if it is close, lean toward \`approved-with-revisions\` over \`requires-another-review\`. Reserve \`requires-another-review\` for genuine issues that the editor has not addressed despite prior feedback.`;
46
+ }
47
+ async function callReviewer(args) {
48
+ const result = await args.runner({
49
+ systemPrompt: SPEC_REVIEWER_PROMPT,
50
+ userMessage: args.userMessage,
51
+ model: args.model,
52
+ tools: ["Read", "Grep", "Glob"],
53
+ addDirs: args.addDirs,
54
+ jsonSchema: SPEC_REVIEW_JSON_SCHEMA,
55
+ session: {
56
+ kind: args.isFirstTurn ? "fresh" : "resume",
57
+ sessionId: args.sessionId,
58
+ },
59
+ });
60
+ if (result.exitCode !== 0) {
61
+ throw new Error(`Spec reviewer failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
62
+ }
63
+ // Retry-once on parse failure (CTS-EDIT-1 convention, same as outline loop).
64
+ try {
65
+ return parseSpecReview(result.stdout);
66
+ }
67
+ catch (err) {
68
+ const retry = await args.runner({
69
+ systemPrompt: SPEC_REVIEWER_PROMPT,
70
+ userMessage: `${args.userMessage}\n\n(Your prior output failed schema validation: ${err instanceof Error ? err.message : String(err)}. Please try again, emitting JSON only.)`,
71
+ model: args.model,
72
+ tools: ["Read", "Grep", "Glob"],
73
+ addDirs: args.addDirs,
74
+ jsonSchema: SPEC_REVIEW_JSON_SCHEMA,
75
+ session: { kind: "resume", sessionId: args.sessionId },
76
+ });
77
+ if (retry.exitCode !== 0) {
78
+ throw new Error(`Spec reviewer retry failed (exit ${retry.exitCode}): ${retry.stderr || retry.stdout}`);
79
+ }
80
+ return parseSpecReview(retry.stdout);
81
+ }
82
+ }
83
+ export async function runEditLoop(options) {
84
+ const { composedSpecPath, specVersionFor, fullPackPath, addDirs, maxTurns = DEFAULT_EDIT_LOOP_MAX_TURNS, runner = runClaude, model, progress, sessionId = randomUUID(), } = options;
85
+ // Turn 1: copy the composed spec to spec-v1 and review it.
86
+ const specV1 = specVersionFor(1);
87
+ copyFileSync(composedSpecPath, specV1);
88
+ progress?.emit({
89
+ stage: "spec-review",
90
+ step: "turn-1",
91
+ message: `Reviewing composed spec (${specV1})`,
92
+ });
93
+ let review = await callReviewer({
94
+ runner,
95
+ sessionId,
96
+ isFirstTurn: true,
97
+ userMessage: reviewerUserMessageInitial(specV1, fullPackPath),
98
+ addDirs,
99
+ model,
100
+ });
101
+ const specPathsByTurn = [specV1];
102
+ const reviewsByTurn = [review];
103
+ let currentSpecPath = specV1;
104
+ progress?.emit({
105
+ stage: "spec-review",
106
+ step: "turn-1",
107
+ message: `Verdict: ${review.verdict}`,
108
+ data: { verdict: review.verdict },
109
+ });
110
+ let turn = 1;
111
+ while (review.verdict === "requires-another-review" && turn < maxTurns) {
112
+ turn++;
113
+ const nextSpec = specVersionFor(turn);
114
+ progress?.emit({
115
+ stage: "spec-review",
116
+ step: `edit-turn-${turn}`,
117
+ message: `Editor revising spec (turn ${turn}/${maxTurns})`,
118
+ });
119
+ const editResult = await runEditor({
120
+ inputSpecPath: currentSpecPath,
121
+ outputSpecPath: nextSpec,
122
+ fullPackPath,
123
+ addDirs,
124
+ critique: review,
125
+ mode: "revise",
126
+ runner,
127
+ model,
128
+ });
129
+ if (!editResult.wroteSpec) {
130
+ throw new Error(`Editor produced no output spec at ${nextSpec} (exit ${editResult.exitCode}): ${editResult.stderr}`);
131
+ }
132
+ currentSpecPath = nextSpec;
133
+ specPathsByTurn.push(currentSpecPath);
134
+ const nudge = turn >= maxTurns - 1 && turn !== maxTurns
135
+ ? convergenceNudge(turn, maxTurns)
136
+ : undefined;
137
+ progress?.emit({
138
+ stage: "spec-review",
139
+ step: `turn-${turn}`,
140
+ message: `Reviewing revised spec (turn ${turn}/${maxTurns})`,
141
+ data: { turn, maxTurns, nudge: !!nudge },
142
+ });
143
+ review = await callReviewer({
144
+ runner,
145
+ sessionId,
146
+ isFirstTurn: false,
147
+ userMessage: reviewerUserMessageRevised(turn, currentSpecPath, fullPackPath, nudge),
148
+ addDirs,
149
+ model,
150
+ });
151
+ reviewsByTurn.push(review);
152
+ progress?.emit({
153
+ stage: "spec-review",
154
+ step: `turn-${turn}`,
155
+ message: `Verdict: ${review.verdict}`,
156
+ data: { verdict: review.verdict },
157
+ });
158
+ }
159
+ const hitMaxTurns = review.verdict === "requires-another-review" && turn >= maxTurns;
160
+ // approved-with-revisions → apply mechanical revisions to a final version.
161
+ if (review.verdict === "approved-with-revisions" &&
162
+ review.revisions.length > 0) {
163
+ const finalTurn = turn + 1;
164
+ const finalPath = specVersionFor(finalTurn);
165
+ progress?.emit({
166
+ stage: "spec-review",
167
+ step: "apply-revisions",
168
+ message: `Applying ${review.revisions.length} mechanical revision(s)`,
169
+ });
170
+ const applyResult = await runEditor({
171
+ inputSpecPath: currentSpecPath,
172
+ outputSpecPath: finalPath,
173
+ fullPackPath,
174
+ addDirs,
175
+ critique: review,
176
+ mode: "apply",
177
+ runner,
178
+ model,
179
+ });
180
+ if (!applyResult.wroteSpec) {
181
+ throw new Error(`Apply-mode editor produced no output spec at ${finalPath} (exit ${applyResult.exitCode}): ${applyResult.stderr}`);
182
+ }
183
+ currentSpecPath = finalPath;
184
+ specPathsByTurn.push(currentSpecPath);
185
+ }
186
+ return {
187
+ finalSpecPath: currentSpecPath,
188
+ specPathsByTurn,
189
+ reviewsByTurn,
190
+ finalVerdict: review.verdict,
191
+ turnsUsed: turn,
192
+ hitMaxTurns,
193
+ };
194
+ }
195
+ //# sourceMappingURL=edit-loop.js.map