sequant 2.8.0 → 2.10.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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +23 -4
  4. package/dist/bin/cli.js +99 -18
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +58 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +38 -10
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +250 -344
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +4 -2
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
  28. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  29. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  30. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +11 -3
  31. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  34. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  35. package/dist/src/commands/assess-render.d.ts +23 -0
  36. package/dist/src/commands/assess-render.js +60 -0
  37. package/dist/src/commands/doctor.js +8 -13
  38. package/dist/src/commands/init.js +13 -1
  39. package/dist/src/commands/locks.js +2 -0
  40. package/dist/src/commands/logs.js +20 -6
  41. package/dist/src/commands/merge.js +154 -2
  42. package/dist/src/commands/ready-tui-adapter.js +6 -1
  43. package/dist/src/commands/ready.d.ts +25 -3
  44. package/dist/src/commands/ready.js +39 -13
  45. package/dist/src/commands/resume.d.ts +113 -0
  46. package/dist/src/commands/resume.js +193 -0
  47. package/dist/src/commands/run-display.d.ts +20 -0
  48. package/dist/src/commands/run-display.js +117 -5
  49. package/dist/src/commands/run-flags.d.ts +42 -1
  50. package/dist/src/commands/run-flags.js +53 -1
  51. package/dist/src/commands/run-progress.js +26 -1
  52. package/dist/src/commands/run.js +11 -10
  53. package/dist/src/commands/state.js +7 -0
  54. package/dist/src/commands/stats.js +47 -0
  55. package/dist/src/commands/status.d.ts +9 -0
  56. package/dist/src/commands/status.js +24 -1
  57. package/dist/src/commands/sync.js +90 -19
  58. package/dist/src/commands/update.js +28 -5
  59. package/dist/src/lib/ac-parser.d.ts +14 -0
  60. package/dist/src/lib/ac-parser.js +99 -5
  61. package/dist/src/lib/assess/index.d.ts +10 -0
  62. package/dist/src/lib/assess/index.js +9 -0
  63. package/dist/src/lib/assess/renderer.d.ts +64 -0
  64. package/dist/src/lib/assess/renderer.js +481 -0
  65. package/dist/src/lib/assess/types.d.ts +224 -0
  66. package/dist/src/lib/assess/types.js +241 -0
  67. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  68. package/dist/src/lib/assess-collision-detect.js +68 -4
  69. package/dist/src/lib/ci/outputs.d.ts +15 -2
  70. package/dist/src/lib/ci/outputs.js +17 -5
  71. package/dist/src/lib/cli-flags.d.ts +33 -0
  72. package/dist/src/lib/cli-flags.js +46 -0
  73. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  74. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  75. package/dist/src/lib/cli-ui/run-renderer.js +79 -9
  76. package/dist/src/lib/errors.d.ts +84 -6
  77. package/dist/src/lib/errors.js +142 -17
  78. package/dist/src/lib/locks/index.d.ts +3 -3
  79. package/dist/src/lib/locks/index.js +2 -2
  80. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  81. package/dist/src/lib/locks/lock-manager.js +47 -3
  82. package/dist/src/lib/locks/types.d.ts +34 -3
  83. package/dist/src/lib/locks/types.js +16 -0
  84. package/dist/src/lib/manifest.js +1 -17
  85. package/dist/src/lib/mcp-config.d.ts +45 -0
  86. package/dist/src/lib/mcp-config.js +77 -1
  87. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  88. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  89. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  90. package/dist/src/lib/merge-check/command-result.js +56 -0
  91. package/dist/src/lib/merge-check/index.d.ts +4 -0
  92. package/dist/src/lib/merge-check/index.js +1 -1
  93. package/dist/src/lib/merge-check/report.js +5 -1
  94. package/dist/src/lib/merge-check/types.d.ts +9 -0
  95. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  96. package/dist/src/lib/merge-check/watch.js +215 -0
  97. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  98. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  99. package/dist/src/lib/settings.d.ts +14 -0
  100. package/dist/src/lib/settings.js +61 -8
  101. package/dist/src/lib/shutdown.d.ts +42 -2
  102. package/dist/src/lib/shutdown.js +75 -5
  103. package/dist/src/lib/skills-check.d.ts +26 -0
  104. package/dist/src/lib/skills-check.js +31 -0
  105. package/dist/src/lib/stacks.d.ts +110 -1
  106. package/dist/src/lib/stacks.js +244 -5
  107. package/dist/src/lib/templates.d.ts +74 -0
  108. package/dist/src/lib/templates.js +171 -16
  109. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  110. package/dist/src/lib/test-tautology-detector.js +153 -12
  111. package/dist/src/lib/version-check.d.ts +31 -0
  112. package/dist/src/lib/version-check.js +46 -7
  113. package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
  114. package/dist/src/lib/workflow/batch-executor.js +550 -54
  115. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  116. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  117. package/dist/src/lib/workflow/chain-resume.d.ts +118 -0
  118. package/dist/src/lib/workflow/chain-resume.js +170 -0
  119. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  120. package/dist/src/lib/workflow/completed-status.js +57 -0
  121. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  122. package/dist/src/lib/workflow/config-resolver.js +54 -2
  123. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  124. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  125. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  126. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  127. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  128. package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
  129. package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
  130. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  131. package/dist/src/lib/workflow/error-classifier.js +14 -1
  132. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  133. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  134. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  135. package/dist/src/lib/workflow/heartbeat.js +90 -0
  136. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  137. package/dist/src/lib/workflow/log-writer.js +81 -14
  138. package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
  139. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  140. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  141. package/dist/src/lib/workflow/phase-executor.d.ts +234 -13
  142. package/dist/src/lib/workflow/phase-executor.js +533 -56
  143. package/dist/src/lib/workflow/phase-registry.js +2 -1
  144. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  145. package/dist/src/lib/workflow/platforms/github.js +152 -0
  146. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  147. package/dist/src/lib/workflow/qa-cache.js +22 -9
  148. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  149. package/dist/src/lib/workflow/ready-gate.js +25 -9
  150. package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
  151. package/dist/src/lib/workflow/run-log-schema.js +92 -2
  152. package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
  153. package/dist/src/lib/workflow/run-orchestrator.js +429 -47
  154. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  155. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  156. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  157. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  158. package/dist/src/lib/workflow/state-manager.js +83 -0
  159. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  160. package/dist/src/lib/workflow/state-schema.js +47 -0
  161. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  162. package/dist/src/lib/workflow/status-derivation.js +27 -0
  163. package/dist/src/lib/workflow/types.d.ts +149 -4
  164. package/dist/src/lib/workflow/types.js +1 -0
  165. package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
  166. package/dist/src/lib/workflow/worktree-manager.js +201 -52
  167. package/dist/src/mcp/tools/run.d.ts +11 -1
  168. package/dist/src/mcp/tools/run.js +22 -1
  169. package/dist/src/mcp/tools/status.js +6 -0
  170. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  171. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  172. package/dist/src/ui/tui/IssueBox.js +10 -2
  173. package/dist/src/ui/tui/row-cap.js +2 -1
  174. package/dist/src/ui/tui/theme.d.ts +7 -0
  175. package/dist/src/ui/tui/theme.js +9 -0
  176. package/package.json +9 -10
  177. package/templates/hooks/parallel-marker.sh +50 -0
  178. package/templates/hooks/post-tool.sh +38 -10
  179. package/templates/hooks/pre-tool.sh +405 -67
  180. package/templates/scripts/cleanup-worktree.sh +282 -30
  181. package/templates/scripts/new-feature.sh +264 -16
  182. package/templates/skills/_shared/references/trust-model.md +18 -0
  183. package/templates/skills/assess/SKILL.md +250 -344
  184. package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
  185. package/templates/skills/docs/SKILL.md +4 -4
  186. package/templates/skills/exec/SKILL.md +26 -6
  187. package/templates/skills/fullsolve/SKILL.md +4 -2
  188. package/templates/skills/loop/SKILL.md +38 -2
  189. package/templates/skills/merger/SKILL.md +2 -2
  190. package/templates/skills/qa/SKILL.md +337 -57
  191. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  192. package/templates/skills/qa/references/call-site-review.md +2 -2
  193. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  194. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  195. package/templates/skills/qa/references/quality-gates.md +21 -7
  196. package/templates/skills/qa/references/testing-requirements.md +1 -1
  197. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  198. package/templates/skills/reflect/SKILL.md +27 -13
  199. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  200. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  201. package/templates/skills/release/SKILL.md +11 -3
  202. package/templates/skills/setup/SKILL.md +5 -5
  203. package/templates/skills/spec/SKILL.md +2 -0
  204. package/templates/skills/spec/references/verification-criteria.md +1 -1
  205. package/templates/skills/test/SKILL.md +1 -1
  206. package/dist/src/lib/phase-spinner.d.ts +0 -146
  207. package/dist/src/lib/phase-spinner.js +0 -255
  208. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  209. package/dist/src/lib/workflow/pr-operations.js +0 -326
  210. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  211. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -58,6 +58,18 @@ export function buildProgressWiring(args) {
58
58
  // #543: activity events only feed the TUI's nowLine — skip the line renderer.
59
59
  if (event === "activity")
60
60
  return;
61
+ // #804 AC-7: unlike activity, a waiting event MUST reach the renderer —
62
+ // it is the only signal during a pause that can last hours.
63
+ if (event === "waiting") {
64
+ renderer.onEvent({
65
+ issue,
66
+ phase,
67
+ event,
68
+ text: extra?.text,
69
+ wakeAtMs: extra?.wakeAtMs,
70
+ });
71
+ return;
72
+ }
61
73
  // #624 Item 3: pass the outer-loop iteration through so the renderer can
62
74
  // render `(attempt N/M)` / `loop N/M`.
63
75
  renderer.onEvent({
@@ -71,9 +83,22 @@ export function buildProgressWiring(args) {
71
83
  };
72
84
  }
73
85
  else if (heartbeat) {
74
- onProgress = (issue, phase, event) => {
86
+ onProgress = (issue, phase, event, extra) => {
75
87
  if (event === "activity")
76
88
  return;
89
+ // #804 AC-7: mark/clear the auto-wait so the heartbeat reports the wait
90
+ // and, critically, does not report it as a stall. Must be handled before
91
+ // the `else` below, which would otherwise `stop()` the phase entirely and
92
+ // leave a multi-hour wait with no liveness signal at all.
93
+ if (event === "waiting") {
94
+ if (extra?.wakeAtMs !== undefined) {
95
+ heartbeat.pauseForWait({ issueNumber: issue, phase }, extra.wakeAtMs);
96
+ }
97
+ else {
98
+ heartbeat.resumeFromWait({ issueNumber: issue, phase });
99
+ }
100
+ return;
101
+ }
77
102
  if (event === "start")
78
103
  heartbeat.start({
79
104
  issueNumber: issue,
@@ -8,15 +8,21 @@ import { parseBatches } from "../lib/workflow/batch-executor.js";
8
8
  import { RunOrchestrator } from "../lib/workflow/run-orchestrator.js";
9
9
  import { displayConfig, displaySummary } from "./run-display.js";
10
10
  import { buildProgressWiring } from "./run-progress.js";
11
- import { normalizeQualityLoop, resolveTuiEnabled } from "./run-flags.js";
11
+ import { normalizeQualityLoop, resolveTuiEnabled, warnDeprecatedFlags, } from "./run-flags.js";
12
12
  // Re-export public API for backwards compatibility
13
13
  export * from "./run-compat.js";
14
+ // #848: red pre-flight rejection + non-zero exit (process.exitCode, not exit()).
15
+ function rejectPreflight(message) {
16
+ console.log(chalk.red(message));
17
+ process.exitCode = 1;
18
+ }
14
19
  /** Parse CLI args → validate → delegate to RunOrchestrator.run() → display summary. */
15
20
  export async function runCommand(issues, options) {
16
21
  // #705: fold the hidden `-q` alias into qualityLoop before any consumer reads
17
22
  // it (`-q` no longer maps to --quiet, which moved to `-s`). See run-flags.ts.
18
23
  options.qualityLoop = normalizeQualityLoop(options);
19
24
  console.log(ui.headerBox("SEQUANT WORKFLOW"));
25
+ warnDeprecatedFlags(options);
20
26
  if (!options.quiet) {
21
27
  try {
22
28
  const v = await checkVersionCached();
@@ -31,31 +37,26 @@ export async function runCommand(issues, options) {
31
37
  }
32
38
  const manifest = await getManifest();
33
39
  if (!manifest) {
34
- console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
40
+ rejectPreflight("❌ Sequant is not initialized. Run `sequant init` first.");
35
41
  return;
36
42
  }
37
43
  const settings = await getSettings();
38
44
  // #605: --stacked implies --chain; reject explicit --no-chain combo before
39
45
  // we evaluate any --chain-dependent constraint below.
40
46
  if (options.stacked && options.chain === false) {
41
- console.log(chalk.red("❌ --stacked cannot be combined with --no-chain"));
47
+ rejectPreflight("❌ --stacked cannot be combined with --no-chain");
42
48
  return;
43
49
  }
44
50
  if (options.stacked) {
45
51
  options.chain = true;
46
52
  }
47
- // Validate constraints
48
53
  if (options.chain && options.batch?.length) {
49
- console.log(chalk.red("❌ --chain cannot be used with --batch"));
54
+ rejectPreflight("❌ --chain cannot be used with --batch");
50
55
  return;
51
56
  }
52
57
  if (options.concurrency !== undefined &&
53
58
  (options.concurrency < 1 || !Number.isInteger(options.concurrency))) {
54
- console.log(chalk.red(`❌ Invalid --concurrency value: ${options.concurrency}. Must be a positive integer.`));
55
- return;
56
- }
57
- if (options.qaGate && !options.chain) {
58
- console.log(chalk.red("❌ --qa-gate requires --chain flag"));
59
+ rejectPreflight(`❌ Invalid --concurrency value: ${options.concurrency}. Must be a positive integer.`);
59
60
  return;
60
61
  }
61
62
  let batches = null;
@@ -27,10 +27,13 @@ export async function stateInitCommand(options = {}) {
27
27
  const result = await discoverUntrackedWorktrees(discoverOptions);
28
28
  if (options.json) {
29
29
  console.log(JSON.stringify(result, null, 2));
30
+ if (!result.success)
31
+ process.exitCode = 1;
30
32
  return;
31
33
  }
32
34
  if (!result.success) {
33
35
  console.log(chalk.red(`✗ Discovery failed: ${result.error}`));
36
+ process.exitCode = 1;
34
37
  return;
35
38
  }
36
39
  if (result.discovered.length === 0) {
@@ -106,6 +109,7 @@ export async function stateRebuildCommand(options = {}) {
106
109
  else {
107
110
  console.log(chalk.red(`✗ Log rebuild failed: ${logResult.error}`));
108
111
  }
112
+ process.exitCode = 1;
109
113
  return;
110
114
  }
111
115
  // Step 2: Discover and add untracked worktrees
@@ -204,10 +208,13 @@ export async function stateCleanCommand(options = {}) {
204
208
  });
205
209
  if (options.json) {
206
210
  console.log(JSON.stringify(result, null, 2));
211
+ if (!result.success)
212
+ process.exitCode = 1;
207
213
  return;
208
214
  }
209
215
  if (!result.success) {
210
216
  console.log(chalk.red(`✗ Cleanup failed: ${result.error}`));
217
+ process.exitCode = 1;
211
218
  return;
212
219
  }
213
220
  const orphanedCount = result.orphaned.length;
@@ -263,6 +263,35 @@ function loadMetrics() {
263
263
  return null;
264
264
  }
265
265
  }
266
+ /**
267
+ * Compute the failure-category breakdown over runs that recorded a failure —
268
+ * outcome "failed" (every issue failed) or "partial" (at least one issue
269
+ * failed) (#783).
270
+ *
271
+ * NOTE — deviation from AC-1's literal "over failed runs" wording: partial runs
272
+ * also carry a failureCategory (recorded whenever >=1 issue fails) and are
273
+ * genuine signal for "what's killing my runs", so they are counted here by
274
+ * explicit user decision. Success runs never carry the field and are excluded.
275
+ *
276
+ * Runs without the field (pre-#761 records, or a partial whose failure was
277
+ * never categorized) are bucketed as "unclassified" — never dropped and never
278
+ * conflated with the "unknown" enum value.
279
+ *
280
+ * Pure function of the passed runs array, so any upstream cohort filtering is
281
+ * automatically respected (computed after filtering, not before).
282
+ */
283
+ function calculateFailureCategoryBreakdown(runs) {
284
+ const counts = new Map();
285
+ for (const run of runs) {
286
+ if (run.outcome === "success")
287
+ continue;
288
+ const category = run.failureCategory ?? "unclassified";
289
+ counts.set(category, (counts.get(category) ?? 0) + 1);
290
+ }
291
+ return [...counts.entries()]
292
+ .map(([category, count]) => ({ category, count }))
293
+ .sort((a, b) => b.count - a.count || a.category.localeCompare(b.category));
294
+ }
266
295
  /**
267
296
  * Calculate analytics from metrics
268
297
  */
@@ -274,6 +303,7 @@ function calculateMetricsAnalytics(metrics) {
274
303
  successCount: 0,
275
304
  partialCount: 0,
276
305
  failedCount: 0,
306
+ failureCategories: [],
277
307
  successRate: 0,
278
308
  avgTokensPerRun: 0,
279
309
  avgFilesChanged: 0,
@@ -294,6 +324,7 @@ function calculateMetricsAnalytics(metrics) {
294
324
  const successCount = runs.filter((r) => r.outcome === "success").length;
295
325
  const partialCount = runs.filter((r) => r.outcome === "partial").length;
296
326
  const failedCount = runs.filter((r) => r.outcome === "failed").length;
327
+ const failureCategories = calculateFailureCategoryBreakdown(runs);
297
328
  const successRate = (successCount / runs.length) * 100;
298
329
  const avgTokensPerRun = runs.reduce((sum, r) => sum + r.metrics.tokensUsed, 0) / runs.length;
299
330
  const avgFilesChanged = runs.reduce((sum, r) => sum + r.metrics.filesChanged, 0) / runs.length;
@@ -323,6 +354,7 @@ function calculateMetricsAnalytics(metrics) {
323
354
  successCount,
324
355
  partialCount,
325
356
  failedCount,
357
+ failureCategories,
326
358
  successRate,
327
359
  avgTokensPerRun,
328
360
  avgFilesChanged,
@@ -415,6 +447,19 @@ function displayMetricsAnalytics(analytics) {
415
447
  const failedRate = (analytics.failedCount / total) * 100;
416
448
  console.log(` ${colors.error("\u2717 Failed")} ${analytics.failedCount} (${failedRate.toFixed(0)}%) ${failedBar}`);
417
449
  }
450
+ // Failure-category breakdown (#783) \u2014 adjacent to the outcome bars above.
451
+ // Counts runs that recorded a failure (outcome "failed" or "partial"); hidden
452
+ // entirely when there are none (AC-3).
453
+ if (analytics.failureCategories.length > 0) {
454
+ const failureRunCount = analytics.failureCategories.reduce((sum, b) => sum + b.count, 0);
455
+ const runNoun = failureRunCount === 1 ? "run with a failure" : "runs with a failure";
456
+ console.log(ui.sectionHeader(`Failure Categories (${failureRunCount} ${runNoun})`));
457
+ const pad = Math.max(...analytics.failureCategories.map((b) => b.category.length));
458
+ for (const bucket of analytics.failureCategories) {
459
+ const pct = ((bucket.count / failureRunCount) * 100).toFixed(0);
460
+ console.log(` ${bucket.category.padEnd(pad)} ${bucket.count} (${pct}%)`);
461
+ }
462
+ }
418
463
  // Averages table
419
464
  console.log(ui.sectionHeader("Averages"));
420
465
  const avgData = {};
@@ -653,6 +698,8 @@ export async function statsCommand(options) {
653
698
  partialCount: analytics.partialCount,
654
699
  failedCount: analytics.failedCount,
655
700
  successRate: analytics.successRate,
701
+ // Failure-category breakdown over failed runs (#783 AC-5)
702
+ failureCategories: analytics.failureCategories,
656
703
  avgTokensPerRun: analytics.avgTokensPerRun,
657
704
  avgFilesChanged: analytics.avgFilesChanged,
658
705
  avgLinesAdded: analytics.avgLinesAdded,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * sequant status - Show version, configuration, and workflow state
3
3
  */
4
+ import { type IssueState } from "../lib/workflow/state-schema.js";
4
5
  export interface StatusCommandOptions {
5
6
  /** Show only issues state */
6
7
  issues?: boolean;
@@ -21,4 +22,12 @@ export interface StatusCommandOptions {
21
22
  /** Skip GitHub API queries (offline mode) */
22
23
  offline?: boolean;
23
24
  }
25
+ /**
26
+ * Format a single issue state for display
27
+ *
28
+ * @internal Exported for testing only (#892: the windowHalt/autoWait pause
29
+ * lines are display contract — a halted issue must read as resumable, not as
30
+ * a bare failure).
31
+ */
32
+ export declare function formatIssueState(issue: IssueState): string;
24
33
  export declare function statusCommand(options?: StatusCommandOptions): Promise<void>;
@@ -2,6 +2,8 @@
2
2
  * sequant status - Show version, configuration, and workflow state
3
3
  */
4
4
  import chalk from "chalk";
5
+ import { formatResetTime } from "../lib/errors.js";
6
+ import { MAX_RESUME_REENTRIES } from "../lib/workflow/state-schema.js";
5
7
  import { ui, colors } from "../lib/cli-ui.js";
6
8
  import { getManifest, getPackageVersion } from "../lib/manifest.js";
7
9
  import { fileExists } from "../lib/fs.js";
@@ -102,8 +104,12 @@ function getPhaseSymbol(phaseState) {
102
104
  }
103
105
  /**
104
106
  * Format a single issue state for display
107
+ *
108
+ * @internal Exported for testing only (#892: the windowHalt/autoWait pause
109
+ * lines are display contract — a halted issue must read as resumable, not as
110
+ * a bare failure).
105
111
  */
106
- function formatIssueState(issue) {
112
+ export function formatIssueState(issue) {
107
113
  const lines = [];
108
114
  // Issue header
109
115
  lines.push(chalk.bold(` #${issue.number}: ${issue.title.substring(0, 50)}${issue.title.length > 50 ? "..." : ""}`));
@@ -128,6 +134,16 @@ function formatIssueState(issue) {
128
134
  .join(" ");
129
135
  lines.push(` Phases: ${phaseProgress}`);
130
136
  lines.push(` ${phases.map((p) => p.charAt(0).toUpperCase()).join(" ")}`);
137
+ // #860: an auto-waiting issue is paused on purpose, not stalled — name the
138
+ // wake time so a multi-hour rate-limit pause is distinguishable from a hang.
139
+ if (issue.autoWait) {
140
+ lines.push(chalk.yellow(` ⏸ Auto-wait: ${issue.autoWait.phase} paused — resuming at ${formatResetTime(new Date(issue.autoWait.wakeAt).getTime())}`));
141
+ }
142
+ // #892: a window-halted issue exited cleanly (lock released) and is waiting
143
+ // for `sequant resume` — show the resume time instead of a bare failure.
144
+ if (issue.windowHalt) {
145
+ lines.push(chalk.yellow(` ⏸ Halted: ${issue.windowHalt.phase} hit a rate-limit window — resumable at ${formatResetTime(new Date(issue.windowHalt.resumeAt).getTime())} via \`sequant resume\` (re-entries ${issue.windowHalt.reentries}/${MAX_RESUME_REENTRIES})`));
146
+ }
131
147
  // PR info
132
148
  if (issue.pr) {
133
149
  lines.push(chalk.gray(` PR: #${issue.pr.number} ${issue.pr.url}`));
@@ -419,6 +435,7 @@ async function displayIssueState(options) {
419
435
  else {
420
436
  console.log(chalk.red(`\nError reading state: ${error}`));
421
437
  }
438
+ process.exitCode = 1;
422
439
  }
423
440
  }
424
441
  /**
@@ -431,6 +448,8 @@ async function handleRebuild(options) {
431
448
  const result = await rebuildStateFromLogs({ verbose: !options.json });
432
449
  if (options.json) {
433
450
  console.log(JSON.stringify(result, null, 2));
451
+ if (!result.success)
452
+ process.exitCode = 1;
434
453
  return;
435
454
  }
436
455
  if (result.success) {
@@ -443,6 +462,7 @@ async function handleRebuild(options) {
443
462
  }
444
463
  else {
445
464
  console.log(chalk.red(`✗ Rebuild failed: ${result.error}`));
465
+ process.exitCode = 1;
446
466
  }
447
467
  }
448
468
  /**
@@ -470,6 +490,8 @@ async function handleCleanup(options) {
470
490
  });
471
491
  if (options.json) {
472
492
  console.log(JSON.stringify(result, null, 2));
493
+ if (!result.success)
494
+ process.exitCode = 1;
473
495
  return;
474
496
  }
475
497
  if (result.success) {
@@ -511,5 +533,6 @@ async function handleCleanup(options) {
511
533
  }
512
534
  else {
513
535
  console.log(chalk.red(`✗ Cleanup failed: ${result.error}`));
536
+ process.exitCode = 1;
514
537
  }
515
538
  }
@@ -8,8 +8,10 @@ import chalk from "chalk";
8
8
  import { join } from "path";
9
9
  import { createHash } from "crypto";
10
10
  import { getManifest, updateManifest, getPackageVersion, } from "../lib/manifest.js";
11
- import { copyTemplates, computeTemplateChanges, listTemplateFiles, getTemplatesDir, } from "../lib/templates.js";
11
+ import { copyTemplates, computeTemplateChanges, listTemplateFiles, getTemplatesDir, assertTemplatesDirExists, isCustomizableFile, } from "../lib/templates.js";
12
12
  import { getConfig } from "../lib/config.js";
13
+ import { resolveCliInvocation } from "../lib/version-check.js";
14
+ import { syncSequantMcpPin } from "../lib/mcp-config.js";
13
15
  import { writeFile, readFile, fileExists, getFileStats } from "../lib/fs.js";
14
16
  import { generateAgentsMd, writeAgentsMd, AGENTS_MD_PATH, } from "../lib/agents-md.js";
15
17
  import { getProjectName } from "../lib/project-name.js";
@@ -195,6 +197,18 @@ export async function syncCommand(options = {}) {
195
197
  process.exitCode = 1;
196
198
  return;
197
199
  }
200
+ // Fail loudly on a missing templates root before anything reads or writes.
201
+ // This must precede the version fast path below: that path diffs against the
202
+ // same (missing) directory, sees no drift, and reports "already up to date"
203
+ // over an empty tree — the second-run trap in #822.
204
+ try {
205
+ await assertTemplatesDirExists();
206
+ }
207
+ catch (error) {
208
+ console.log(chalk.red(`❌ ${error.message}`));
209
+ process.exitCode = 1;
210
+ return;
211
+ }
198
212
  const packageVersion = getPackageVersion();
199
213
  const skillsVersion = await getSkillsVersion();
200
214
  if (!quiet) {
@@ -205,6 +219,18 @@ export async function syncCommand(options = {}) {
205
219
  // Get config tokens for template processing
206
220
  const config = await getConfig();
207
221
  const tokens = config?.tokens || {};
222
+ // Re-pin the project .mcp.json MCP server to the installed version (#793).
223
+ // `syncSequantMcpPin`'s contract has always named this the `update`/`sync`
224
+ // path, but only `update` ever called it, so a `sync` left the pin stale.
225
+ // Placed here for the same reason update.ts states: it must precede the
226
+ // "already up to date" fast path below, because a version-only upgrade
227
+ // leaves every template byte-identical and returns early — and that is
228
+ // exactly the case where the pin most needs refreshing.
229
+ const mcpPin = syncSequantMcpPin(process.cwd(), { dryRun });
230
+ if (mcpPin.updated && !quiet) {
231
+ const verb = dryRun ? "Would update" : "Updated";
232
+ console.log(chalk.blue(`${verb} .mcp.json MCP pin: ${mcpPin.from} → ${mcpPin.to}`));
233
+ }
208
234
  // The version marker is only a fast-path hint — verify actual content before
209
235
  // claiming "up to date". On a version match we still diff bundled templates
210
236
  // against installed content (rendered with the same variables) so we never
@@ -233,25 +259,36 @@ export async function syncCommand(options = {}) {
233
259
  }
234
260
  // Preview path: report exactly what the apply would write, then stop without
235
261
  // mutating (#722). This branch is only reached when `force` is set or the
236
- // version marker mismatches — i.e. the path that runs `copyTemplates(force:
237
- // true)` and rewrites the whole tree. (A matching-version, non-force dry-run
238
- // already returned at the report-only short-circuit above, which never
239
- // mutates.) `copyTemplates` does NOT protect in-place customizations the way
240
- // `update` does the force copy overwrites them so the preview counts
241
- // `local-override` files alongside `new`/`modified`. Reporting only
242
- // new+modified would under-report the write-set, the exact divergence #722
243
- // is about.
262
+ // version marker mismatches — i.e. the path that runs `copyTemplates` and
263
+ // rewrites the whole tree. (A matching-version, non-force dry-run already
264
+ // returned at the report-only short-circuit above, which never mutates.)
265
+ //
266
+ // Since #814 the apply path PRESERVES in-place customizations (files in
267
+ // CUSTOMIZABLE_FILES) under a plain sync and only overwrites them under an
268
+ // explicit `--force`. The preview must mirror that split exactly, or it would
269
+ // re-introduce the dry-run/apply divergence #722 is about: partition the
270
+ // `local-override` set into preserved (plain sync) vs overwritten (--force,
271
+ // or a non-customizable `.local`-twin the tree copy still rewrites).
244
272
  if (dryRun) {
245
273
  const changes = await computeTemplateChanges(manifest.stack, tokens);
246
274
  const newFiles = changes.filter((c) => c.status === "new");
247
275
  const modifiedFiles = changes.filter((c) => c.status === "modified");
248
276
  const localOverrides = changes.filter((c) => c.status === "local-override");
249
- const toWrite = [...newFiles, ...modifiedFiles, ...localOverrides];
277
+ const preservedOverrides = force
278
+ ? []
279
+ : localOverrides.filter((c) => isCustomizableFile(c.path));
280
+ const overwrittenOverrides = localOverrides.filter((c) => force || !isCustomizableFile(c.path));
281
+ const toWrite = [...newFiles, ...modifiedFiles, ...overwrittenOverrides];
250
282
  if (!quiet) {
251
283
  console.log(chalk.bold("Summary (dry-run):"));
252
284
  console.log(chalk.green(` New files: ${newFiles.length}`));
253
285
  console.log(chalk.yellow(` Modified: ${modifiedFiles.length}`));
254
- console.log(chalk.blue(` Local overrides (overwritten by sync): ${localOverrides.length}`));
286
+ if (preservedOverrides.length > 0) {
287
+ console.log(chalk.blue(` Customizable (preserved): ${preservedOverrides.length}`));
288
+ }
289
+ if (overwrittenOverrides.length > 0) {
290
+ console.log(chalk.blue(` Local overrides (overwritten): ${overwrittenOverrides.length}`));
291
+ }
255
292
  if (modifiedFiles.length > 0) {
256
293
  console.log(chalk.bold("\nModified files:"));
257
294
  for (const file of modifiedFiles) {
@@ -264,9 +301,15 @@ export async function syncCommand(options = {}) {
264
301
  console.log(chalk.green(` ${file.path}`));
265
302
  }
266
303
  }
267
- if (localOverrides.length > 0) {
268
- console.log(chalk.bold("\nLocal overrides (will be overwritten by sync):"));
269
- for (const file of localOverrides) {
304
+ if (preservedOverrides.length > 0) {
305
+ console.log(chalk.bold("\nCustomizable files (preserved run `sync --force` to replace):"));
306
+ for (const file of preservedOverrides) {
307
+ console.log(chalk.blue(` ${file.path}`));
308
+ }
309
+ }
310
+ if (overwrittenOverrides.length > 0) {
311
+ console.log(chalk.bold("\nLocal overrides (will be overwritten):"));
312
+ for (const file of overwrittenOverrides) {
270
313
  console.log(chalk.blue(` ${file.path}`));
271
314
  }
272
315
  }
@@ -286,14 +329,39 @@ export async function syncCommand(options = {}) {
286
329
  }
287
330
  return;
288
331
  }
289
- // Copy templates with force to overwrite existing files
332
+ // Copy templates: always refresh the managed trees, but only overwrite
333
+ // user-owned CUSTOMIZABLE_FILES (e.g. the constitution) when the user
334
+ // explicitly passed --force. These are two distinct notions of "force" and
335
+ // conflating them is what silently ate the constitution (#814).
290
336
  const copyOptions = {
291
- force: true, // Always overwrite when syncing
337
+ force: true, // Always overwrite the managed skills/agents/hooks trees
338
+ overwriteCustomizable: force, // Clobber user-owned files only on explicit --force
292
339
  };
340
+ // AC-3: under --force, announce which customizable files are about to be
341
+ // overwritten *before* writing them, so the destructive action is never
342
+ // silent. Reuse the diff path's `local-override` classification (the same
343
+ // set --dry-run reports), narrowed to CUSTOMIZABLE_FILES.
344
+ if (force && !quiet) {
345
+ const changes = await computeTemplateChanges(manifest.stack, tokens);
346
+ const overwrites = changes.filter((c) => c.status === "local-override" && isCustomizableFile(c.path));
347
+ if (overwrites.length > 0) {
348
+ console.log(chalk.yellow(`Overwriting ${overwrites.length} customizable file(s) (--force):`));
349
+ for (const file of overwrites) {
350
+ console.log(chalk.yellow(` ${file.path}`));
351
+ }
352
+ }
353
+ }
293
354
  if (!quiet) {
294
355
  console.log(chalk.blue("Copying templates..."));
295
356
  }
296
- await copyTemplates(manifest.stack, tokens, copyOptions);
357
+ const { preservedCustomizable } = await copyTemplates(manifest.stack, tokens, copyOptions);
358
+ // AC-4: on a plain sync, report each customizable file that was preserved so
359
+ // the apply path's output matches what --dry-run already promises (#722).
360
+ if (!quiet && preservedCustomizable.length > 0) {
361
+ for (const file of preservedCustomizable) {
362
+ console.log(chalk.blue(` preserved: ${file} — run \`sync --force\` to replace`));
363
+ }
364
+ }
297
365
  // Update version markers
298
366
  await updateSkillsVersion();
299
367
  await updateManifest();
@@ -340,9 +408,12 @@ export async function syncCommand(options = {}) {
340
408
  */
341
409
  export async function checkAndWarnSkillsOutdated(status) {
342
410
  const { outdated, currentVersion, packageVersion, contentDrift } = status ?? (await areSkillsOutdated());
411
+ // Name the invocation that reaches *this* build rather than assuming the
412
+ // documented `npm install sequant` shape — see resolveCliInvocation.
413
+ const cli = resolveCliInvocation();
343
414
  if (outdated) {
344
415
  console.log(chalk.yellow(`\n! Skills are outdated (${currentVersion || "unknown"} → ${packageVersion})`));
345
- console.log(chalk.yellow(" Run: npx sequant sync\n"));
416
+ console.log(chalk.yellow(` Run: ${cli} sync\n`));
346
417
  return true;
347
418
  }
348
419
  if (contentDrift > 0) {
@@ -350,7 +421,7 @@ export async function checkAndWarnSkillsOutdated(status) {
350
421
  // version is report-only (it won't copy), so point at the commands that
351
422
  // actually resolve in-place drift — `sync --force` or `update`.
352
423
  console.log(chalk.yellow(`\n! Version current, but ${contentDrift} file(s) differ from bundled content`));
353
- console.log(chalk.yellow(" Run: npx sequant sync --force (or npx sequant update)\n"));
424
+ console.log(chalk.yellow(` Run: ${cli} sync --force (or ${cli} update)\n`));
354
425
  return true;
355
426
  }
356
427
  return false;
@@ -7,9 +7,11 @@ import { spawnSync } from "child_process";
7
7
  import { getManifest, updateManifest, getPackageVersion, } from "../lib/manifest.js";
8
8
  import { computeTemplateChanges } from "../lib/templates.js";
9
9
  import { getConfig, saveConfig } from "../lib/config.js";
10
- import { getStackConfig, PM_CONFIG, getPackageManagerCommands, } from "../lib/stacks.js";
10
+ import { getStackConfig, PM_CONFIG, getPackageManagerCommands, resolvePackageManager, } from "../lib/stacks.js";
11
11
  import { writeFile } from "../lib/fs.js";
12
+ import { chmod } from "fs/promises";
12
13
  import { isStdinTTY, isCI, getNonInteractiveReason } from "../lib/tty.js";
14
+ import { syncSequantMcpPin } from "../lib/mcp-config.js";
13
15
  /**
14
16
  * True when `update` must not prompt: stdin is not a terminal (piped input) or
15
17
  * we are running in a recognized CI environment. CI is checked explicitly
@@ -41,8 +43,18 @@ export async function updateCommand(options) {
41
43
  const manifest = await getManifest();
42
44
  if (!manifest) {
43
45
  console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
46
+ process.exitCode = 1;
44
47
  return;
45
48
  }
49
+ // Resolved once for the whole command. The manifest's packageManager is a
50
+ // snapshot taken at `sequant init`, so it is absent on pre-1.3.0 installs —
51
+ // exactly the population the PM_RUN backfill below exists to serve. The
52
+ // three sites that used to spell this `(manifest.packageManager as keyof
53
+ // typeof PM_CONFIG) || "npm"` therefore assumed npm for those installs, and
54
+ // wrote an npm PM_RUN token into a pnpm/yarn/bun project's config (#870).
55
+ // The root is the cwd because `update` operates on the project in place,
56
+ // the same directory `getManifest` and `syncSequantMcpPin` read.
57
+ const pm = resolvePackageManager(manifest.packageManager, process.cwd());
46
58
  const packageVersion = getPackageVersion();
47
59
  console.log(chalk.gray(`Current version: ${manifest.version}`));
48
60
  console.log(chalk.gray(`Stack: ${manifest.stack}`));
@@ -64,7 +76,6 @@ export async function updateCommand(options) {
64
76
  console.log(chalk.gray(`Dev URL: ${tokens.DEV_URL || "(not set)"}\n`));
65
77
  // Add PM_RUN if missing (for existing installs before v1.3.0)
66
78
  if (!tokens.PM_RUN) {
67
- const pm = manifest.packageManager || "npm";
68
79
  const pmConfig = getPackageManagerCommands(pm);
69
80
  tokens.PM_RUN = pmConfig.run;
70
81
  config.tokens = tokens;
@@ -78,7 +89,6 @@ export async function updateCommand(options) {
78
89
  const stackConfig = getStackConfig(manifest.stack);
79
90
  const defaultDevUrl = stackConfig.devUrl;
80
91
  // Get package manager run command
81
- const pm = manifest.packageManager || "npm";
82
92
  const pmConfig = getPackageManagerCommands(pm);
83
93
  if (options.force || options.yes) {
84
94
  tokens = { DEV_URL: defaultDevUrl, PM_RUN: pmConfig.run };
@@ -116,6 +126,16 @@ export async function updateCommand(options) {
116
126
  console.log(chalk.green("✔ Configuration saved\n"));
117
127
  }
118
128
  }
129
+ // Re-pin the project .mcp.json MCP server to the installed version (#793).
130
+ // Independent of template changes: a version-only upgrade leaves every
131
+ // template unchanged (so the flow would short-circuit at "Everything is up to
132
+ // date" below) but should still refresh the pin so the MCP server tracks the
133
+ // release the user just updated to. Runs here, before that short-circuit.
134
+ const mcpPin = syncSequantMcpPin(process.cwd(), { dryRun: options.dryRun });
135
+ if (mcpPin.updated) {
136
+ const verb = options.dryRun ? "Would update" : "Updated";
137
+ console.log(chalk.blue(`${verb} .mcp.json MCP pin: ${mcpPin.from} → ${mcpPin.to}`));
138
+ }
119
139
  // Compute changes using the shared, variable-aware comparison.
120
140
  // Templates are rendered (PROJECT_NAME, STACK_NOTES, etc.) before diffing,
121
141
  // and in-place-customizable files (constitution) are protected as overrides.
@@ -202,6 +222,11 @@ export async function updateCommand(options) {
202
222
  let updated = 0;
203
223
  for (const file of applySet) {
204
224
  await writeFile(file.path, file.rendered);
225
+ // Shell scripts (hooks, scripts/dev) need the exec bit, matching the
226
+ // copyTemplates write path.
227
+ if (file.path.endsWith(".sh")) {
228
+ await chmod(file.path, 0o755);
229
+ }
205
230
  updated++;
206
231
  }
207
232
  // Update manifest
@@ -210,8 +235,6 @@ export async function updateCommand(options) {
210
235
  // Check if package.json was updated and run install
211
236
  const packageJsonUpdated = applySet.some((f) => f.path === "package.json" || f.path.endsWith("/package.json"));
212
237
  if (packageJsonUpdated) {
213
- // Use detected package manager or default to npm
214
- const pm = manifest.packageManager || "npm";
215
238
  const pmConfig = PM_CONFIG[pm];
216
239
  console.log(chalk.blue(`\npackage.json updated, running ${pmConfig.install}...`));
217
240
  const [cmd, ...args] = pmConfig.install.split(" ");
@@ -6,6 +6,12 @@
6
6
  * Also supports alternate formats: `- [ ] **B2:** Description`
7
7
  * And bold-wrapped format: `- [ ] **AC-1: Description**`
8
8
  *
9
+ * Bare checkbox items (`- [ ] Some requirement`, no ID prefix) are also
10
+ * honored, but ONLY under an explicit `## Acceptance Criteria` heading, where
11
+ * they receive synthesized stable IDs. Human-authored issues rarely prefix
12
+ * their ACs with IDs, so without this every hand-written issue parsed to zero
13
+ * ACs — blinding every downstream AC consumer (#850).
14
+ *
9
15
  * @example
10
16
  * ```typescript
11
17
  * import { parseAcceptanceCriteria } from './ac-parser';
@@ -39,8 +45,16 @@ export declare function inferVerificationMethod(description: string): ACVerifica
39
45
  * - `- [ ] **AC-1:** Description`
40
46
  * - `- [ ] **B2:** Description`
41
47
  * - `- [ ] **AC-1: Description**` (bold wraps ID + description)
48
+ * - `- [ ] **AC-1** Description` (bold ID, no colon)
49
+ * - `- [ ] **AC-1**: Description` (colon outside the bold)
42
50
  * - `- [ ] AC-1: Description`
43
51
  *
52
+ * Bare checkbox items without an ID prefix (`- [ ] Some requirement`) are also
53
+ * parsed, but ONLY under an explicit `## Acceptance Criteria` heading, where
54
+ * they receive synthesized stable IDs (`AC-1`, `AC-2`, ...). This keeps
55
+ * unrelated checklists elsewhere in the body (Open Questions, Test Plan) from
56
+ * being misread as ACs, while letting ordinary human-written issues parse.
57
+ *
44
58
  * @param issueBody - The full GitHub issue body markdown
45
59
  * @returns Array of parsed acceptance criteria
46
60
  */