@popoverai/dotrequirements 0.26.0 → 0.26.2

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 (104) hide show
  1. package/dist/codebase-to-spec/area-name.d.ts +13 -0
  2. package/dist/codebase-to-spec/area-name.js +18 -0
  3. package/dist/codebase-to-spec/cache.d.ts +31 -0
  4. package/dist/codebase-to-spec/cache.js +29 -1
  5. package/dist/codebase-to-spec/compose.d.ts +7 -4
  6. package/dist/codebase-to-spec/compose.js +10 -21
  7. package/dist/codebase-to-spec/dispatch.js +1 -1
  8. package/dist/codebase-to-spec/present.js +11 -7
  9. package/dist/codebase-to-spec/prompts/planner-initial.d.ts +3 -2
  10. package/dist/codebase-to-spec/prompts/planner-initial.js +3 -2
  11. package/dist/codebase-to-spec/renumber.d.ts +52 -0
  12. package/dist/codebase-to-spec/renumber.js +105 -0
  13. package/dist/codebase-to-spec/schemas.d.ts +35 -240
  14. package/dist/codebase-to-spec/schemas.js +5 -173
  15. package/dist/codebase-to-spec/validate.js +3 -2
  16. package/dist/commands/acceptance-test.js +4 -2
  17. package/dist/commands/ai-setup.js +97 -59
  18. package/dist/commands/codebase-to-spec/dispatch-editor.js +1 -1
  19. package/dist/commands/codebase-to-spec/dispatch-spec.js +1 -1
  20. package/dist/commands/codebase-to-spec/index.js +7 -103
  21. package/dist/commands/codebase-to-spec/pack.js +8 -1
  22. package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +3 -1
  23. package/dist/commands/codebase-to-spec/present-orchestrator.js +11 -4
  24. package/dist/commands/get.js +6 -2
  25. package/dist/commands/init.js +7 -5
  26. package/dist/commands/link-resolution.d.ts +3 -1
  27. package/dist/commands/link-resolution.js +4 -2
  28. package/dist/commands/pull.js +36 -3
  29. package/dist/commands/push.js +54 -16
  30. package/dist/commands/report.js +18 -3
  31. package/dist/commands/review-test.js +16 -8
  32. package/dist/commands/tests-for.js +13 -13
  33. package/dist/commands/validate.js +14 -14
  34. package/dist/harness/cache.d.ts +19 -3
  35. package/dist/harness/cache.js +38 -12
  36. package/dist/harness/finalize.js +33 -1
  37. package/dist/harness/index.js +16 -9
  38. package/dist/harness/requirementsLoader.js +12 -0
  39. package/dist/harness/tracking.d.ts +17 -2
  40. package/dist/harness/tracking.js +83 -9
  41. package/dist/mcp/handlers/authoring.js +13 -4
  42. package/dist/mcp/handlers/get.js +7 -3
  43. package/dist/mcp/handlers/push.js +59 -13
  44. package/dist/mcp/handlers/review.d.ts +1 -0
  45. package/dist/mcp/handlers/review.js +58 -15
  46. package/dist/mcp/handlers/test-mapping.js +47 -12
  47. package/dist/mcp/handlers/types.d.ts +14 -0
  48. package/dist/mcp/handlers/types.js +27 -0
  49. package/dist/mcp/index.js +4 -0
  50. package/dist/push/core.d.ts +50 -0
  51. package/dist/push/core.js +149 -11
  52. package/dist/push/index.d.ts +1 -1
  53. package/dist/push/index.js +1 -1
  54. package/dist/requirements/cloud-coverage.d.ts +12 -2
  55. package/dist/requirements/cloud-coverage.js +30 -3
  56. package/dist/requirements/grep.d.ts +7 -2
  57. package/dist/requirements/grep.js +75 -47
  58. package/dist/schema/builder.d.ts +1 -1
  59. package/dist/schema/builder.js +13 -0
  60. package/dist/schema/conversions.d.ts +7 -2
  61. package/dist/schema/conversions.js +13 -4
  62. package/dist/schema/parser-core.d.ts +28 -0
  63. package/dist/schema/parser-core.js +80 -9
  64. package/dist/schema/parser.d.ts +8 -26
  65. package/dist/schema/parser.js +23 -251
  66. package/dist/schema/resolver.js +18 -8
  67. package/dist/templates/skills/codebase-to-spec/SKILL.md +10 -4
  68. package/dist/utils/env.js +17 -1
  69. package/dist/utils/oauth-flow.js +8 -0
  70. package/dist/utils/project-settings.d.ts +4 -0
  71. package/dist/utils/project-settings.js +14 -1
  72. package/package.json +1 -1
  73. package/dist/codebase-to-spec/edit-loop.d.ts +0 -54
  74. package/dist/codebase-to-spec/edit-loop.js +0 -195
  75. package/dist/codebase-to-spec/editor.d.ts +0 -54
  76. package/dist/codebase-to-spec/editor.js +0 -74
  77. package/dist/codebase-to-spec/fan-out.d.ts +0 -63
  78. package/dist/codebase-to-spec/fan-out.js +0 -215
  79. package/dist/codebase-to-spec/outline-review-loop.d.ts +0 -51
  80. package/dist/codebase-to-spec/outline-review-loop.js +0 -187
  81. package/dist/codebase-to-spec/planner.d.ts +0 -41
  82. package/dist/codebase-to-spec/planner.js +0 -76
  83. package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +0 -12
  84. package/dist/codebase-to-spec/prompts/outline-reviewer.js +0 -89
  85. package/dist/codebase-to-spec/slice.d.ts +0 -49
  86. package/dist/codebase-to-spec/slice.js +0 -111
  87. package/dist/codebase-to-spec/specifier.d.ts +0 -60
  88. package/dist/codebase-to-spec/specifier.js +0 -85
  89. package/dist/codebase-to-spec/summary.d.ts +0 -51
  90. package/dist/codebase-to-spec/summary.js +0 -183
  91. package/dist/commands/codebase-to-spec/compose.d.ts +0 -14
  92. package/dist/commands/codebase-to-spec/compose.js +0 -57
  93. package/dist/commands/codebase-to-spec/edit-loop.d.ts +0 -16
  94. package/dist/commands/codebase-to-spec/edit-loop.js +0 -83
  95. package/dist/commands/codebase-to-spec/fan-out.d.ts +0 -19
  96. package/dist/commands/codebase-to-spec/fan-out.js +0 -77
  97. package/dist/commands/codebase-to-spec/plan-loop.d.ts +0 -26
  98. package/dist/commands/codebase-to-spec/plan-loop.js +0 -105
  99. package/dist/commands/codebase-to-spec/present.d.ts +0 -26
  100. package/dist/commands/codebase-to-spec/present.js +0 -97
  101. package/dist/commands/codebase-to-spec/run.d.ts +0 -20
  102. package/dist/commands/codebase-to-spec/run.js +0 -86
  103. package/dist/commands/codebase-to-spec/specify-area.d.ts +0 -18
  104. package/dist/commands/codebase-to-spec/specify-area.js +0 -82
@@ -87,8 +87,9 @@ function describeValidationError(err) {
87
87
  const parts = [err.message];
88
88
  let cursor = err.cause;
89
89
  while (cursor instanceof Error) {
90
- // Don't repeat the same message twice if the wrapper just rethrew.
91
- if (cursor.message && cursor.message !== parts[parts.length - 1]) {
90
+ // Don't repeat the same message twice if the wrapper just rethrew
91
+ // (or already embedded the inner message in its own).
92
+ if (cursor.message && !parts[parts.length - 1].includes(cursor.message)) {
92
93
  parts.push(cursor.message);
93
94
  }
94
95
  cursor = cursor.cause;
@@ -127,8 +127,10 @@ export async function acceptanceTestCommand(requirementKey, url, options) {
127
127
  else {
128
128
  displayResults(requirementTree, results);
129
129
  }
130
- // 11. Exit with appropriate code
131
- const failedCount = results.filter((r) => r.status !== "passed").length;
130
+ // 11. Exit with appropriate code — computed over the requirement tree so
131
+ // a requirement with a missing result counts as not passing, agreeing
132
+ // with the displayed counts (ACCEPTANCE-1.0/.1/.2)
133
+ const failedCount = requirementTree.filter((_, i) => results[i]?.status !== "passed").length;
132
134
  if (failedCount > 0) {
133
135
  process.exit(1);
134
136
  }
@@ -1,10 +1,11 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync, execSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import prompts from "prompts";
6
6
  import { brand } from "../utils/brand.js";
7
7
  import { appendOrUpdateSection, buildNoGitRepoMessage, findDotrequirementsSection, findGitRoot, getContextFileName, } from "../utils/context-file.js";
8
+ import { findProjectRoot, readProjectSettings, } from "../utils/project-settings.js";
8
9
  import { loadTemplate } from "../utils/templates.js";
9
10
  /**
10
11
  * Load the context file section template
@@ -12,6 +13,56 @@ import { loadTemplate } from "../utils/templates.js";
12
13
  function loadContextFileSection() {
13
14
  return loadTemplate("context-file-section.md");
14
15
  }
16
+ /**
17
+ * Read an assistant's existing MCP config file (AISETUP-2).
18
+ * Returns {} when the file is absent or empty. Returns null when the file
19
+ * exists but is unusable — unreadable (AISETUP-2.2), invalid JSON
20
+ * (AISETUP-2.1), or a non-object root (AISETUP-2.3). In every null case the
21
+ * path has already been shown to the user and the caller must stop without
22
+ * writing; rewriting would destroy every other configured server.
23
+ */
24
+ function readExistingMcpConfig(configPath) {
25
+ const stopWithoutWriting = (reason) => {
26
+ console.error(`❌ ${reason}`);
27
+ console.log(" Nothing was changed. Fix (or remove) the file, then re-run ai-setup:");
28
+ console.log(` ${configPath}\n`);
29
+ return null;
30
+ };
31
+ if (!existsSync(configPath)) {
32
+ return {};
33
+ }
34
+ let existingContent;
35
+ try {
36
+ existingContent = readFileSync(configPath, "utf-8");
37
+ }
38
+ catch {
39
+ return stopWithoutWriting("Existing config file could not be read.");
40
+ }
41
+ if (!existingContent.trim()) {
42
+ return {};
43
+ }
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(existingContent);
47
+ }
48
+ catch {
49
+ return stopWithoutWriting("Existing config file contains invalid JSON.");
50
+ }
51
+ // AISETUP-2.3: a non-object root (e.g. a top-level array) would swallow the
52
+ // mcpServers assignment and serialize back without it — success would be
53
+ // reported while nothing was configured
54
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
55
+ return stopWithoutWriting("Existing config file is not a JSON object.");
56
+ }
57
+ // AISETUP-2.3: same trap one level down — an array-valued mcpServers takes
58
+ // the server entry as an expando property that JSON.stringify drops
59
+ const mcpServers = parsed.mcpServers;
60
+ if (mcpServers != null &&
61
+ (typeof mcpServers !== "object" || Array.isArray(mcpServers))) {
62
+ return stopWithoutWriting('Existing config file has an "mcpServers" field that is not a JSON object.');
63
+ }
64
+ return parsed;
65
+ }
15
66
  /**
16
67
  * Install workflow guidance to the appropriate context file for a platform.
17
68
  * With `yes` (the non-interactive --assistant path, AISETUP-1.0), both
@@ -166,10 +217,18 @@ async function dispatchSetup(client, yes = false) {
166
217
  }
167
218
  async function setupClaudeCode(yes = false) {
168
219
  console.log("\n📦 Configuring MCP for Claude Code...\n");
169
- const command = `claude mcp add-json dotrequirements '{"type":"stdio","command":"npx","args":["-y","@popoverai/dotrequirements","mcp"]}'`;
220
+ // Register via an args array so no shell quoting is involved: cmd.exe does
221
+ // not honor single quotes, so a shell-quoted JSON string breaks on Windows.
222
+ const mcpServerJson = JSON.stringify({
223
+ type: "stdio",
224
+ command: "npx",
225
+ args: ["-y", "@popoverai/dotrequirements", "mcp"],
226
+ });
227
+ // Shell-safe on every platform (no quoting needed anywhere)
228
+ const manualCommand = "claude mcp add dotrequirements -- npx -y @popoverai/dotrequirements mcp";
170
229
  let mcpConfigured = false;
171
230
  try {
172
- execSync(command, { stdio: "pipe" });
231
+ execFileSync("claude", ["mcp", "add-json", "dotrequirements", mcpServerJson], { stdio: "pipe" });
173
232
  console.log("✅ MCP configured successfully for Claude Code!");
174
233
  mcpConfigured = true;
175
234
  }
@@ -185,7 +244,7 @@ async function setupClaudeCode(yes = false) {
185
244
  else {
186
245
  console.error("❌ Failed to configure MCP automatically.");
187
246
  console.log("\nYou can manually run:");
188
- console.log(` ${command}`);
247
+ console.log(` ${manualCommand}`);
189
248
  if (stderr) {
190
249
  console.log(`\nError: ${stderr}`);
191
250
  }
@@ -224,24 +283,17 @@ async function setupClaudeDesktop() {
224
283
  REQUIREMENTS_DIR: process.cwd(),
225
284
  },
226
285
  };
286
+ // AISETUP-2.1–2.3: bail out before touching anything if the config is unusable
287
+ const config = readExistingMcpConfig(configPath);
288
+ if (config === null) {
289
+ return;
290
+ }
227
291
  try {
228
292
  if (!existsSync(configDir)) {
229
293
  console.log("Creating Claude Desktop config directory...");
230
294
  mkdirSync(configDir, { recursive: true });
231
295
  }
232
- let config = {};
233
- if (existsSync(configPath)) {
234
- try {
235
- const existingContent = readFileSync(configPath, "utf-8");
236
- if (existingContent.trim()) {
237
- config = JSON.parse(existingContent);
238
- }
239
- }
240
- catch {
241
- console.log("⚠️ Existing config file is invalid, creating fresh config...");
242
- config = {};
243
- }
244
- }
296
+ // AISETUP-2.0: other configured servers survive; only ours is (re)set
245
297
  if (!config.mcpServers) {
246
298
  config.mcpServers = {};
247
299
  }
@@ -271,30 +323,26 @@ async function setupClaudeDesktop() {
271
323
  }
272
324
  async function setupCursor(yes = false) {
273
325
  console.log("\n📦 Configuring MCP for Cursor (project-specific)...\n");
274
- const configPath = join(process.cwd(), ".cursor", "mcp.json");
275
- const configDir = join(process.cwd(), ".cursor");
326
+ // Cursor loads .cursor/mcp.json from the project (git) root — match the
327
+ // context-file placement, falling back to cwd outside a repository
328
+ const projectDir = (await findGitRoot()) ?? process.cwd();
329
+ const configDir = join(projectDir, ".cursor");
330
+ const configPath = join(configDir, "mcp.json");
276
331
  const mcpServerConfig = {
277
332
  command: "npx",
278
333
  args: ["-y", "@popoverai/dotrequirements", "mcp"],
279
334
  };
335
+ // AISETUP-2.1–2.3: bail out before touching anything if the config is unusable
336
+ const config = readExistingMcpConfig(configPath);
337
+ if (config === null) {
338
+ return;
339
+ }
280
340
  try {
281
341
  if (!existsSync(configDir)) {
282
342
  console.log("Creating Cursor config directory...");
283
343
  mkdirSync(configDir, { recursive: true });
284
344
  }
285
- let config = {};
286
- if (existsSync(configPath)) {
287
- try {
288
- const existingContent = readFileSync(configPath, "utf-8");
289
- if (existingContent.trim()) {
290
- config = JSON.parse(existingContent);
291
- }
292
- }
293
- catch {
294
- console.log("⚠️ Existing config file is invalid, creating fresh config...");
295
- config = {};
296
- }
297
- }
345
+ // AISETUP-2.0: other configured servers survive; only ours is (re)set
298
346
  if (!config.mcpServers) {
299
347
  config.mcpServers = {};
300
348
  }
@@ -324,45 +372,33 @@ async function setupAntigravity(yes = false) {
324
372
  console.log("\n📦 Configuring MCP for Google Antigravity...\n");
325
373
  const configPath = join(homedir(), ".gemini", "antigravity", "mcp_config.json");
326
374
  const configDir = join(homedir(), ".gemini", "antigravity");
327
- // Check if we're in a dotrequirements project directory
328
- const envLocalPath = join(process.cwd(), ".env.local");
375
+ // AISETUP-3.0: detect the project the way the CLI defines projects —
376
+ // .requirements/project-settings.json at the project root
329
377
  let projectId = null;
330
378
  let projectPath = null;
331
- if (existsSync(envLocalPath)) {
379
+ const projectRoot = findProjectRoot();
380
+ if (projectRoot) {
332
381
  try {
333
- const envContent = readFileSync(envLocalPath, "utf-8");
334
- const lines = envContent.split("\n");
335
- for (const line of lines) {
336
- const trimmed = line.trim();
337
- if (trimmed.startsWith("DOTREQUIREMENTS_PROJECT_ID=")) {
338
- projectId = trimmed.split("=")[1]?.trim().replace(/['"]/g, "");
339
- projectPath = process.cwd();
340
- break;
341
- }
382
+ const settings = readProjectSettings(projectRoot);
383
+ if (settings) {
384
+ projectId = settings.projectId;
385
+ projectPath = projectRoot;
342
386
  }
343
387
  }
344
388
  catch {
345
- // .env.local exists but couldn't read it - continue with global setup
389
+ // Settings file unreadable/invalid — continue with global setup
346
390
  }
347
391
  }
392
+ // AISETUP-2.1–2.3: bail out before touching anything if the config is unusable
393
+ const config = readExistingMcpConfig(configPath);
394
+ if (config === null) {
395
+ return;
396
+ }
348
397
  try {
349
398
  if (!existsSync(configDir)) {
350
399
  console.log("Creating Antigravity config directory...");
351
400
  mkdirSync(configDir, { recursive: true });
352
401
  }
353
- let config = {};
354
- if (existsSync(configPath)) {
355
- try {
356
- const existingContent = readFileSync(configPath, "utf-8");
357
- if (existingContent.trim()) {
358
- config = JSON.parse(existingContent);
359
- }
360
- }
361
- catch {
362
- console.log("⚠️ Existing config file is invalid, creating fresh config...");
363
- config = {};
364
- }
365
- }
366
402
  if (!config.mcpServers) {
367
403
  config.mcpServers = {};
368
404
  }
@@ -385,7 +421,9 @@ async function setupAntigravity(yes = false) {
385
421
  console.log(` Path: ${projectPath}\n`);
386
422
  }
387
423
  else {
388
- console.log(`ℹ️ Not in a ${brand} project directory (no .env.local with DOTREQUIREMENTS_PROJECT_ID found)`);
424
+ // AISETUP-3.1: outside any initialized project — skip the per-project
425
+ // step and say so
426
+ console.log("ℹ️ No project found (no .requirements/project-settings.json in this directory or its parents)");
389
427
  console.log(" Setting up global MCP server only.\n");
390
428
  }
391
429
  writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
@@ -12,10 +12,10 @@
12
12
  * - CTSO-CONV-3: orchestrator dispatches an editor when revisions are needed
13
13
  */
14
14
  import { existsSync, readFileSync } from "node:fs";
15
+ import { sanitizeAreaName } from "../../codebase-to-spec/area-name.js";
15
16
  import { cachePaths } from "../../codebase-to-spec/cache.js";
16
17
  import { EDITOR_DISPATCH_ID_PREFIX } from "../../codebase-to-spec/dispatch.js";
17
18
  import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
18
- import { sanitizeAreaName } from "../../codebase-to-spec/fan-out.js";
19
19
  import { parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
20
20
  import { findProjectRoot } from "../../utils/project-settings.js";
21
21
  export async function dispatchEditorCommand(areaPrefix) {
@@ -10,10 +10,10 @@
10
10
  * the orchestrator should approve the outline before fan-out.
11
11
  */
12
12
  import { existsSync, readFileSync } from "node:fs";
13
+ import { sanitizeAreaName } from "../../codebase-to-spec/area-name.js";
13
14
  import { cachePaths } from "../../codebase-to-spec/cache.js";
14
15
  import { SPECIFIER_DISPATCH_ID_PREFIX } from "../../codebase-to-spec/dispatch.js";
15
16
  import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
16
- import { sanitizeAreaName } from "../../codebase-to-spec/fan-out.js";
17
17
  import { parseConversationalOutline, } from "../../codebase-to-spec/schemas.js";
18
18
  import { findProjectRoot } from "../../utils/project-settings.js";
19
19
  export async function dispatchSpecCommand() {
@@ -5,30 +5,23 @@
5
5
  * be invoked granularly (e.g., `dotrequirements cts pack`) per CTS-CLI-2.
6
6
  */
7
7
  import { installCtsAbortHandlers } from "../../codebase-to-spec/progress.js";
8
- import { composeCommand } from "./compose.js";
9
8
  import { composeOrchestratorCommand } from "./compose-orchestrator.js";
10
9
  import { dispatchContextCommand } from "./dispatch-context.js";
11
10
  import { dispatchEditorCommand } from "./dispatch-editor.js";
12
11
  import { dispatchPlannerCommand } from "./dispatch-planner.js";
13
12
  import { dispatchSpecCommand } from "./dispatch-spec.js";
14
- import { editLoopCommand } from "./edit-loop.js";
15
- import { fanOutCommand } from "./fan-out.js";
16
13
  import { packCommand } from "./pack.js";
17
- import { planLoopCommand } from "./plan-loop.js";
18
- import { presentCommand } from "./present.js";
19
14
  import { presentOrchestratorCommand } from "./present-orchestrator.js";
20
- import { runCommand } from "./run.js";
21
15
  import { skillInstallCommand } from "./skill-install.js";
22
- import { specifyAreaCommand } from "./specify-area.js";
23
16
  import { styleCheckCommand } from "./style-check.js";
24
17
  import { validateCommand } from "./validate.js";
25
18
  /**
26
19
  * 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.
20
+ * The conversational orchestrator's plumbing commands (the `dispatch-*` payload
21
+ * builders and the `-orchestrator` compose/present variants) and the
22
+ * skill-install entry point ship in the npm package but aren't surfaced in help
23
+ * listings — they're invoked by name by the orchestrator skill and workflow,
24
+ * not by users, so they stay out of the user-facing CLI surface.
32
25
  */
33
26
  const HIDDEN = { hidden: true };
34
27
  export function registerCodebaseToSpec(program) {
@@ -37,9 +30,8 @@ export function registerCodebaseToSpec(program) {
37
30
  .alias("cts")
38
31
  .description("Generate dotrequirements behavioral specifications from a codebase");
39
32
  // 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.
33
+ // stage emits a structured `pipeline/aborted` line before exiting. preAction
34
+ // fires after argv parsing and before the subcommand action runs.
43
35
  cts.hook("preAction", () => {
44
36
  installCtsAbortHandlers();
45
37
  });
@@ -52,31 +44,6 @@ export function registerCodebaseToSpec(program) {
52
44
  .option("--budget <tokens>", "Override the working-context budget (in tokens)", (v) => parseInt(v, 10))
53
45
  .option("--ignore-requirements", "Exclude `.requirements/**` from the pack (use when testing cts against a codebase whose existing requirements should not influence the output)")
54
46
  .action((opts) => packCommand({ ...opts, budgetTokens: opts.budget }));
55
- cts
56
- .command("plan-loop")
57
- .description("Run the planner + outline review loop on the cached pack")
58
- .option("--max-turns <n>", "Hard cap on review turns", (v) => parseInt(v, 10))
59
- .option("--model <name>", "Override the model used by the planner and reviewer")
60
- .action((opts) => planLoopCommand({
61
- maxTurns: opts.maxTurns,
62
- model: opts.model,
63
- }));
64
- cts
65
- .command("specify-area <name>")
66
- .description("Run the specifier for one named area from the cached outline")
67
- .option("--model <name>", "Override the model used by the specifier")
68
- .action((name, opts) => specifyAreaCommand(name, {
69
- model: opts.model,
70
- }));
71
- cts
72
- .command("fan-out")
73
- .description("Run all specifiers in parallel for areas in the cached outline")
74
- .option("-c, --concurrency <n>", "Max concurrent specifiers", (v) => parseInt(v, 10))
75
- .option("--model <name>", "Override the model used by specifiers")
76
- .action((opts) => fanOutCommand({
77
- concurrency: opts.concurrency,
78
- model: opts.model,
79
- }));
80
47
  cts
81
48
  .command("validate <partial-path>")
82
49
  .description("Validate the schema/syntax of a partial-spec file (deterministic, offline)")
@@ -86,69 +53,6 @@ export function registerCodebaseToSpec(program) {
86
53
  .description("Run a local style-check on a partial-spec file (severity-categorized feedback)")
87
54
  .option("--model <name>", "Override the model used for the check")
88
55
  .action((partialPath, opts) => styleCheckCommand(partialPath, { model: opts.model }));
89
- cts
90
- .command("compose")
91
- .description("Assemble per-area partials into a single composed spec")
92
- .action(() => composeCommand());
93
- cts
94
- .command("edit-loop")
95
- .description("Run the spec reviewer + editor loop on the composed spec")
96
- .option("--max-turns <n>", "Hard cap on edit-loop turns", (v) => parseInt(v, 10))
97
- .option("--model <name>", "Override the model used by the reviewer and editor")
98
- .action((opts) => editLoopCommand({
99
- maxTurns: opts.maxTurns,
100
- model: opts.model,
101
- }));
102
- cts
103
- .command("present")
104
- .description("Write the final spec to .requirements/ and emit a pipeline summary")
105
- .option("--interactive", "Force interactive mode (override TTY detection)")
106
- .option("--non-interactive", "Force non-interactive mode (override TTY detection)")
107
- .option("--overwrite", "Replace existing .requirements/ files without prompting")
108
- .option("--skip-existing", "Leave existing .requirements/ files untouched")
109
- .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)")
110
- .action((opts) => {
111
- const forceMode = opts.interactive
112
- ? "interactive"
113
- : opts.nonInteractive
114
- ? "non-interactive"
115
- : undefined;
116
- return presentCommand({
117
- forceMode,
118
- overwrite: opts.overwrite,
119
- skipExisting: opts.skipExisting,
120
- ignoreRequirements: opts.ignoreRequirements,
121
- });
122
- });
123
- cts
124
- .command("run")
125
- .description("Run the full pipeline (pack → plan → specify → compose → edit → present)")
126
- .option("-s, --scope <path>", "Limit to files under this path (subdir of project root)")
127
- .option("--fresh", "Clear the cache before running")
128
- .option("--budget <tokens>", "Override the working-context budget (in tokens)", (v) => parseInt(v, 10))
129
- .option("--max-turns <n>", "Hard cap on outline review turns", (v) => parseInt(v, 10))
130
- .option("--model <name>", "Override the model used by agent calls")
131
- .option("--interactive", "Force interactive mode for the present stage")
132
- .option("--non-interactive", "Force non-interactive mode for the present stage")
133
- .option("--overwrite", "Replace existing .requirements/ files without prompting")
134
- .option("--skip-existing", "Leave existing .requirements/ files untouched")
135
- .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)")
136
- .action((opts) => {
137
- const forceMode = opts.interactive
138
- ? "interactive"
139
- : opts.nonInteractive
140
- ? "non-interactive"
141
- : undefined;
142
- return runCommand({
143
- ...opts,
144
- budgetTokens: opts.budget,
145
- maxTurns: opts.maxTurns,
146
- forceMode,
147
- overwrite: opts.overwrite,
148
- skipExisting: opts.skipExisting,
149
- ignoreRequirements: opts.ignoreRequirements,
150
- });
151
- });
152
56
  cts
153
57
  .command("dispatch-context <dispatch-id>", HIDDEN)
154
58
  .description("Return the composed prompt for a worker dispatch as JSON on stdout (run by cts-worker subagents to fetch their dispatch prompt)")
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { resolve } from "node:path";
14
14
  import { checkBudget, countTokensInFileExact, DEFAULT_BUDGET_TOKENS, formatBudgetExceededMessage, } from "../../codebase-to-spec/budget.js";
15
- import { clearCache, ensureCacheDir } from "../../codebase-to-spec/cache.js";
15
+ import { clearCache, ensureCacheDir, writeRunConfig, } from "../../codebase-to-spec/cache.js";
16
16
  import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
17
17
  import { runPack } from "../../codebase-to-spec/pack.js";
18
18
  import { StdoutProgress } from "../../codebase-to-spec/progress.js";
@@ -35,6 +35,13 @@ export async function packCommand(rawOptions = {}) {
35
35
  clearCache(projectRoot);
36
36
  }
37
37
  const paths = ensureCacheDir(projectRoot);
38
+ // Record the isolation decision once, here, so later stages (present) honor
39
+ // it without the caller — or the orchestrating agent — having to carry it
40
+ // across the run. Written on every pack so a re-pack without the flag clears
41
+ // a stale isolated marker. See CTS-PRESENT-5.2.
42
+ writeRunConfig(projectRoot, {
43
+ isolated: rawOptions.ignoreRequirements === true,
44
+ });
38
45
  const scope = rawOptions.scope
39
46
  ? resolve(projectRoot, rawOptions.scope)
40
47
  : undefined;
@@ -8,13 +8,15 @@
8
8
  * rather than specFinal.
9
9
  *
10
10
  * Requirements covered:
11
- * - CTS-PRESENT-1..4 (reuses the legacy present logic)
11
+ * - CTS-PRESENT-1..3 (shared present logic in present.ts)
12
12
  * - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
13
13
  */
14
14
  export interface PresentOrchestratorOptions {
15
15
  forceMode?: "interactive" | "non-interactive";
16
16
  overwrite?: boolean;
17
17
  skipExisting?: boolean;
18
+ /** Override the project root, mainly for tests. */
19
+ projectRoot?: string;
18
20
  }
19
21
  export declare function presentOrchestratorCommand(options?: PresentOrchestratorOptions): Promise<void>;
20
22
  //# sourceMappingURL=present-orchestrator.d.ts.map
@@ -8,11 +8,11 @@
8
8
  * rather than specFinal.
9
9
  *
10
10
  * Requirements covered:
11
- * - CTS-PRESENT-1..4 (reuses the legacy present logic)
11
+ * - CTS-PRESENT-1..3 (shared present logic in present.ts)
12
12
  * - CTSO-INTEG-1: orchestrator uses existing deterministic stages unchanged
13
13
  */
14
14
  import { existsSync, readFileSync } from "node:fs";
15
- import { cachePaths } from "../../codebase-to-spec/cache.js";
15
+ import { cachePaths, readRunConfig } from "../../codebase-to-spec/cache.js";
16
16
  import { ExitCode } from "../../codebase-to-spec/exit-codes.js";
17
17
  import { detectMode } from "../../codebase-to-spec/interactive.js";
18
18
  import { runPresent, } from "../../codebase-to-spec/present.js";
@@ -31,7 +31,7 @@ function resolvePolicy(isInteractive, options) {
31
31
  return "fail-fast";
32
32
  }
33
33
  export async function presentOrchestratorCommand(options = {}) {
34
- const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
34
+ const projectRoot = options.projectRoot ?? findProjectRoot(process.cwd()) ?? process.cwd();
35
35
  const paths = cachePaths(projectRoot);
36
36
  if (!existsSync(paths.outline)) {
37
37
  process.stderr.write(`No outline.yaml found at ${paths.outline}. Run the orchestrator's planner first.\n`);
@@ -60,11 +60,17 @@ export async function presentOrchestratorCommand(options = {}) {
60
60
  return;
61
61
  }
62
62
  const legacyOutline = conversationalOutlineToLegacy(outline);
63
+ // Honor the isolation decision recorded at pack time (CTS-PRESENT-5.2). When
64
+ // the run is isolated, write under `.requirements/cts/` so the existing
65
+ // requirements directory is never overwritten — the choice rides the cache,
66
+ // not the agent's memory, so it holds even across a resumed or compacted run.
67
+ const { isolated } = readRunConfig(projectRoot);
63
68
  const result = await runPresent({
64
69
  outline: legacyOutline,
65
70
  finalSpecPath: paths.composedSpec,
66
71
  projectRoot,
67
72
  overwritePolicy: policy,
73
+ outputSubdir: isolated ? "cts" : undefined,
68
74
  });
69
75
  if (!result.allWritten) {
70
76
  if (result.conflictPath) {
@@ -76,6 +82,7 @@ export async function presentOrchestratorCommand(options = {}) {
76
82
  for (const action of result.actions) {
77
83
  process.stdout.write(` ${action.action}: ${action.path}\n`);
78
84
  }
79
- process.stdout.write(`Wrote ${result.actions.filter((a) => a.action === "created" || a.action === "overwrote").length} file(s) to .requirements/\n`);
85
+ const outputDirLabel = isolated ? ".requirements/cts/" : ".requirements/";
86
+ process.stdout.write(`Wrote ${result.actions.filter((a) => a.action === "created" || a.action === "overwrote").length} file(s) to ${outputDirLabel}\n`);
80
87
  }
81
88
  //# sourceMappingURL=present-orchestrator.js.map
@@ -1,10 +1,14 @@
1
1
  import { glob } from "glob";
2
- import { formatRequirementTree, getRequirementTree, loadAllRequirements, } from "../requirements/index.js";
2
+ import { formatRequirementTree, loadAllRequirements, } from "../requirements/index.js";
3
3
  import { findFilesWithRequirement, findTestCodeForRequirement, } from "../requirements/testCodeExtractor.js";
4
4
  export async function getCommand(id) {
5
5
  const workspaceRoot = process.cwd();
6
6
  const { flattened } = await loadAllRequirements(workspaceRoot);
7
- const tree = getRequirementTree(flattened, id);
7
+ // Match the node itself plus all of its descendants by id prefix (mirrors
8
+ // the #46 MCP get fix). The shared getRequirementTree only matches on
9
+ // rootId/id equality, which drops grandchildren (e.g. REQ-123.0.0) when a
10
+ // child id (REQ-123.0) is requested.
11
+ const tree = flattened.filter((r) => r.id === id || r.id.startsWith(`${id}.`));
8
12
  // CLI-FIND-GET-1.2: missing ID → error + non-zero exit
9
13
  if (tree.length === 0) {
10
14
  throw new Error(`Requirement "${id}" not found`);
@@ -147,11 +147,10 @@ async function authenticatedFlow(cwd, options) {
147
147
  }
148
148
  if (projectChoice === "create") {
149
149
  await createNewProjectFlow(cwd, client, selectedTeam, options);
150
+ return true;
150
151
  }
151
- else {
152
- await connectExistingProjectFlow(cwd, client, selectedTeam);
153
- }
154
- return true;
152
+ // INIT-12: a cancelled project selection means init did not complete
153
+ return await connectExistingProjectFlow(cwd, client, selectedTeam);
155
154
  }
156
155
  /**
157
156
  * INIT-18: Prompt user to select a team after OAuth
@@ -268,13 +267,15 @@ async function createNewProjectFlow(cwd, client, team, options) {
268
267
  }
269
268
  /**
270
269
  * INIT-6, INIT-16: Connect to an existing project
270
+ * Returns true if connected successfully, false if the user cancelled
271
+ * (INIT-12: cancellation must not read as a successful init)
271
272
  */
272
273
  async function connectExistingProjectFlow(cwd, client, team) {
273
274
  // INIT-6.0, INIT-16.0: Select project from team
274
275
  const selectedProject = await selectProject(client, team.teamId);
275
276
  if (!selectedProject) {
276
277
  console.log("Initialization cancelled.");
277
- return;
278
+ return false;
278
279
  }
279
280
  // AUTHZ-3: Prompt for secret expiry (paid tier only, if creating new secret)
280
281
  const expiryDays = await promptExpiryDays(team.tier);
@@ -306,6 +307,7 @@ async function connectExistingProjectFlow(cwd, client, team) {
306
307
  console.log("\nNext steps:");
307
308
  console.log(" 1. Review the requirements in .requirements/");
308
309
  console.log(" 2. Add the test harness to your test setup (see README)");
310
+ return true;
309
311
  }
310
312
  /**
311
313
  * MEMBERSHIP-18: Invite flow - join a team using an invite token
@@ -29,7 +29,9 @@ export interface LinkFlags {
29
29
  json?: boolean;
30
30
  }
31
31
  /**
32
- * LINK-12.3: any deviation flag implies non-interactive mode.
32
+ * LINK-12.3: any deviation flag implies non-interactive mode. The JSON
33
+ * output flag does too — machine-readable output only exists on the
34
+ * non-interactive path, so `link --json` alone must not run interactively.
33
35
  */
34
36
  export declare function isNonInteractive(flags: LinkFlags): boolean;
35
37
  export declare function hasCapacity(team: TeamOption): boolean;
@@ -6,10 +6,12 @@
6
6
  * assistant needs to ask the user and retry with a deviation flag.
7
7
  */
8
8
  /**
9
- * LINK-12.3: any deviation flag implies non-interactive mode.
9
+ * LINK-12.3: any deviation flag implies non-interactive mode. The JSON
10
+ * output flag does too — machine-readable output only exists on the
11
+ * non-interactive path, so `link --json` alone must not run interactively.
10
12
  */
11
13
  export function isNonInteractive(flags) {
12
- return Boolean(flags.yes || flags.team || flags.connect || flags.create);
14
+ return Boolean(flags.yes || flags.team || flags.connect || flags.create || flags.json);
13
15
  }
14
16
  export function hasCapacity(team) {
15
17
  return team.projectLimit === -1 || team.projectCount < team.projectLimit;