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
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `sequant assess-render <file>` — render an `AssessResult` JSON payload (#823).
3
+ *
4
+ * Internal surface. The `/assess` skill builds the JSON, calls this, and pastes
5
+ * stdout verbatim. Shipping it as a CLI subcommand rather than a `scripts/`
6
+ * shell-out is what makes the fix reach end users: `skills/spec/SKILL.md:78`
7
+ * establishes that skills may only run `npx tsx` blocks when the sequant source
8
+ * tree is present, so a script would work in this repo and nowhere else. A
9
+ * subcommand ships via `package.json` `bin` + `files: ["dist", ...]` and is
10
+ * present for every sequant install by definition.
11
+ *
12
+ * All output goes to stdout unmodified — no chalk, no boxes, no TTY branching —
13
+ * because the caller pastes it into a chat transcript and a GitHub comment.
14
+ */
15
+ /**
16
+ * Read, validate, and render an `AssessResult` file.
17
+ *
18
+ * Exits non-zero with a readable stderr message when the file is missing,
19
+ * unreadable, not JSON, or fails schema validation — the skill's Step 6 treats a
20
+ * non-zero exit as its signal to fall back to a hand-drawn dashboard rather than
21
+ * silently emitting nothing.
22
+ */
23
+ export declare function assessRenderCommand(file: string): Promise<void>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `sequant assess-render <file>` — render an `AssessResult` JSON payload (#823).
3
+ *
4
+ * Internal surface. The `/assess` skill builds the JSON, calls this, and pastes
5
+ * stdout verbatim. Shipping it as a CLI subcommand rather than a `scripts/`
6
+ * shell-out is what makes the fix reach end users: `skills/spec/SKILL.md:78`
7
+ * establishes that skills may only run `npx tsx` blocks when the sequant source
8
+ * tree is present, so a script would work in this repo and nowhere else. A
9
+ * subcommand ships via `package.json` `bin` + `files: ["dist", ...]` and is
10
+ * present for every sequant install by definition.
11
+ *
12
+ * All output goes to stdout unmodified — no chalk, no boxes, no TTY branching —
13
+ * because the caller pastes it into a chat transcript and a GitHub comment.
14
+ */
15
+ import { readFile } from "node:fs/promises";
16
+ import { render } from "../lib/assess/renderer.js";
17
+ import { AssessResultValidationError, parseAssessResult, } from "../lib/assess/types.js";
18
+ /**
19
+ * Read, validate, and render an `AssessResult` file.
20
+ *
21
+ * Exits non-zero with a readable stderr message when the file is missing,
22
+ * unreadable, not JSON, or fails schema validation — the skill's Step 6 treats a
23
+ * non-zero exit as its signal to fall back to a hand-drawn dashboard rather than
24
+ * silently emitting nothing.
25
+ */
26
+ export async function assessRenderCommand(file) {
27
+ let raw;
28
+ try {
29
+ raw = await readFile(file, "utf-8");
30
+ }
31
+ catch (error) {
32
+ const reason = error instanceof Error ? error.message : String(error);
33
+ console.error(`assess-render: cannot read ${file}\n ${reason}`);
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ let payload;
38
+ try {
39
+ payload = JSON.parse(raw);
40
+ }
41
+ catch (error) {
42
+ const reason = error instanceof Error ? error.message : String(error);
43
+ console.error(`assess-render: ${file} is not valid JSON\n ${reason}`);
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+ try {
48
+ console.log(render(parseAssessResult(payload)));
49
+ }
50
+ catch (error) {
51
+ if (error instanceof AssessResultValidationError) {
52
+ console.error(`assess-render: ${file} — ${error.message}`);
53
+ }
54
+ else {
55
+ const reason = error instanceof Error ? error.message : String(error);
56
+ console.error(`assess-render: failed to render ${file}\n ${reason}`);
57
+ }
58
+ process.exitCode = 1;
59
+ }
60
+ }
@@ -6,10 +6,11 @@ import { execSync } from "child_process";
6
6
  import { ui, colors } from "../lib/cli-ui.js";
7
7
  import { GitHubProvider } from "../lib/workflow/platforms/github.js";
8
8
  import { fileExists, isExecutable } from "../lib/fs.js";
9
+ import { checkSkillsInstalled } from "../lib/skills-check.js";
9
10
  import { getManifest } from "../lib/manifest.js";
10
11
  import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, getMcpServersConfig, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
11
12
  import { getSettings, DEFAULT_AGENT_SETTINGS } from "../lib/settings.js";
12
- import { checkVersionThorough, getVersionWarning, } from "../lib/version-check.js";
13
+ import { checkVersionThorough, getVersionWarning, resolveCliInvocation, } from "../lib/version-check.js";
13
14
  import { areSkillsOutdated } from "./sync.js";
14
15
  import { readAgentsMd, checkAgentsMdConsistency, AGENTS_MD_PATH, } from "../lib/agents-md.js";
15
16
  import { readFile } from "../lib/fs.js";
@@ -146,9 +147,11 @@ export async function doctorCommand(options = {}) {
146
147
  message: "Missing .sequant-manifest.json - run `sequant init`",
147
148
  });
148
149
  }
149
- // Check 2: Skills directory
150
- const skillsExist = await fileExists(".claude/skills");
151
- if (skillsExist) {
150
+ // Checks 2+3: Skills directory + core skills. Shared with the `run`
151
+ // pre-flight via checkSkillsInstalled (#813) so the layout cannot drift.
152
+ const coreSkills = ["spec", "exec", "qa"];
153
+ const { skillsDirExists, missingSkills } = await checkSkillsInstalled(coreSkills);
154
+ if (skillsDirExists) {
152
155
  checks.push({
153
156
  name: "Skills",
154
157
  status: "pass",
@@ -162,14 +165,6 @@ export async function doctorCommand(options = {}) {
162
165
  message: "Missing .claude/skills/ directory",
163
166
  });
164
167
  }
165
- // Check 3: Core skills present
166
- const coreSkills = ["spec", "exec", "qa"];
167
- const missingSkills = [];
168
- for (const skill of coreSkills) {
169
- if (!(await fileExists(`.claude/skills/${skill}/SKILL.md`))) {
170
- missingSkills.push(skill);
171
- }
172
- }
173
168
  if (missingSkills.length === 0) {
174
169
  checks.push({
175
170
  name: "Core Skills",
@@ -190,7 +185,7 @@ export async function doctorCommand(options = {}) {
190
185
  checks.push({
191
186
  name: "Skills Version",
192
187
  status: "warn",
193
- message: `Outdated (${skillsStatus.currentVersion || "unknown"} → ${skillsStatus.packageVersion}) - run: npx sequant sync`,
188
+ message: `Outdated (${skillsStatus.currentVersion || "unknown"} → ${skillsStatus.packageVersion}) - run: ${resolveCliInvocation()} sync`,
194
189
  });
195
190
  }
196
191
  else {
@@ -8,7 +8,7 @@ import { join, dirname } from "path";
8
8
  import { readdir } from "fs/promises";
9
9
  import { ui, colors } from "../lib/cli-ui.js";
10
10
  import { detectStack, detectAllStacks, getStackConfig, detectPackageManager, getPackageManagerCommands, STACKS, } from "../lib/stacks.js";
11
- import { copyTemplates } from "../lib/templates.js";
11
+ import { copyTemplates, assertTemplatesDirExists } from "../lib/templates.js";
12
12
  import { createManifest } from "../lib/manifest.js";
13
13
  import { saveConfig } from "../lib/config.js";
14
14
  import { createDefaultSettings, generateSettingsReference, } from "../lib/settings.js";
@@ -73,6 +73,17 @@ function logDefault(label, value) {
73
73
  console.log(chalk.blue(`${label}: ${value} (default)`));
74
74
  }
75
75
  export async function initCommand(options) {
76
+ // Fail loudly on a missing templates root before any prompt or write, so a
77
+ // broken install can never half-provision a project. Placed ahead of the
78
+ // --upgrade-skills branch, which reads the same directory (#822).
79
+ try {
80
+ await assertTemplatesDirExists();
81
+ }
82
+ catch (error) {
83
+ console.log(chalk.red(`❌ ${error.message}`));
84
+ process.exitCode = 1;
85
+ return;
86
+ }
76
87
  // Handle --upgrade-skills: update skill files from installed package templates
77
88
  if (options.upgradeSkills) {
78
89
  await upgradeSkills();
@@ -520,6 +531,7 @@ async function upgradeSkills() {
520
531
  const installedDir = ".claude/skills";
521
532
  if (!(await fileExists(installedDir))) {
522
533
  console.log(chalk.red("No skills directory found. Run `sequant init` first."));
534
+ process.exitCode = 1;
523
535
  return;
524
536
  }
525
537
  // Resolve the package's templates/skills directory
@@ -14,6 +14,8 @@ function formatSignalLine(issue, pid, result) {
14
14
  return `Refused to signal PID ${pid} for #${issue} (matches this process or its parent)`;
15
15
  case "pid-dead":
16
16
  return `Could not signal PID ${pid} for #${issue} (already exited)`;
17
+ case "stale-pid-untrusted":
18
+ return `Refused to signal PID ${pid} for #${issue} (lock is past the age ceiling; that PID has likely been recycled onto an unrelated process)`;
17
19
  case "kill-failed":
18
20
  return `Could not signal PID ${pid} for #${issue} (kill syscall failed)`;
19
21
  case "orchestrator":
@@ -81,18 +81,32 @@ function formatTime(isoString) {
81
81
  function displayLogSummary(log, filename, options) {
82
82
  const passed = log.summary.passed;
83
83
  const failed = log.summary.failed;
84
+ // #766: `partial` gets its own bucket so an all-partial run isn't rendered as
85
+ // `0 passed, 0 failed` (older logs predate the field → default to 0).
86
+ const partial = log.summary.partial ?? 0;
84
87
  const total = log.summary.totalIssues;
85
- const status = failed > 0
86
- ? chalk.red("FAILED")
87
- : passed === total
88
- ? chalk.green("PASSED")
89
- : chalk.yellow("PARTIAL");
88
+ // #856: an aborted run is a failure, but saying only "FAILED" sends the
89
+ // reader hunting for a bug in their own code. The run never got to fail on
90
+ // its own terms — something killed it.
91
+ const aborted = log.summary.aborted ?? 0;
92
+ const status = aborted > 0
93
+ ? chalk.red("ABORTED")
94
+ : failed > 0
95
+ ? chalk.red("FAILED")
96
+ : passed === total
97
+ ? chalk.green("PASSED")
98
+ : chalk.yellow("PARTIAL");
90
99
  console.log(chalk.blue(`\n Run: ${log.runId.slice(0, 8)}...`));
91
100
  console.log(chalk.gray(` File: ${filename}`));
92
101
  console.log(chalk.gray(` Time: ${formatTime(log.startTime)}`));
93
102
  console.log(chalk.gray(` Duration: ${formatDuration(log.summary.totalDurationSeconds)}`));
94
- console.log(chalk.gray(` Status: ${status} (${passed}/${total} passed, ${failed} failed)`));
103
+ console.log(chalk.gray(` Status: ${status} (${passed}/${total} passed, ${failed} failed` +
104
+ (partial > 0 ? `, ${partial} partial` : "") +
105
+ `)`));
95
106
  console.log(chalk.gray(` Phases: ${log.config.phases.join(" → ")}`));
107
+ if (log.abortedBy) {
108
+ console.log(chalk.red(` Aborted: run terminated by ${log.abortedBy} — results below are incomplete`));
109
+ }
96
110
  // Show issues
97
111
  for (const issue of log.issues) {
98
112
  const issueStatus = issue.status === "success"
@@ -13,8 +13,12 @@
13
13
  */
14
14
  import { spawnSync } from "child_process";
15
15
  import { ui, colors } from "../lib/cli-ui.js";
16
- import { runMergeChecks, formatReportMarkdown, } from "../lib/merge-check/index.js";
16
+ import { runMergeChecks, formatReportMarkdown, resolveBranches, findMostRecentLog, resolveLogDir, } from "../lib/merge-check/index.js";
17
17
  import { LockManager, formatLockedMessage } from "../lib/locks/index.js";
18
+ import { waitForChecks, resolveWatchTiming } from "../lib/merge-check/watch.js";
19
+ import { GitHubProvider } from "../lib/workflow/platforms/github.js";
20
+ /** Exit code for a watch that timed out — distinct from the 0/1/2 verdicts. */
21
+ const EXIT_WATCH_TIMEOUT = 3;
18
22
  /**
19
23
  * Determine exit code from batch verdict
20
24
  */
@@ -43,6 +47,126 @@ function getRepoRoot() {
43
47
  }
44
48
  return result.stdout.trim();
45
49
  }
50
+ /**
51
+ * Resolve the PR numbers to watch for a set of issues.
52
+ *
53
+ * Reuses `resolveBranches` (git remote + worktree scan) for the issue→branch
54
+ * map, then prefers the run-log `prNumber` and falls back to
55
+ * `viewPRByBranchSync`. Issues with no discoverable PR are returned in
56
+ * `unresolved` so the caller can report them rather than silently skipping.
57
+ */
58
+ function resolveWatchTargets(issueNumbers, repoRoot, gh) {
59
+ const runLog = findMostRecentLog(resolveLogDir());
60
+ const branches = resolveBranches(issueNumbers, repoRoot, runLog);
61
+ const targets = [];
62
+ const unresolved = [];
63
+ for (const b of branches) {
64
+ const pr = b.prNumber ?? gh.viewPRByBranchSync(b.branch)?.number ?? null;
65
+ if (pr) {
66
+ targets.push({ issue: b.issueNumber, pr });
67
+ }
68
+ else {
69
+ unresolved.push(b.issueNumber);
70
+ }
71
+ }
72
+ return { targets, unresolved };
73
+ }
74
+ /**
75
+ * Watch gate (#818): before the normal merge-check report runs, poll each
76
+ * resolved PR's CI rollup until every check is terminal.
77
+ *
78
+ * Contract: this **never merges**. It only decides *when* the existing report
79
+ * runs. Returns `true` when it has handled the outcome and the caller must stop
80
+ * (a dispatch block → BLOCKED/exit 2, a timeout → distinct message/exit 3, or no
81
+ * PR could be resolved); returns `false` when every PR reached a terminal state
82
+ * and the caller should fall through to `runMergeChecks` unchanged.
83
+ *
84
+ * Multiple PRs share a single wall-clock deadline so `--timeout` bounds the whole
85
+ * gate, not each PR independently.
86
+ */
87
+ async function runWatchGate(issueNumbers, options, repoRoot) {
88
+ const gh = new GitHubProvider();
89
+ // Auto-detect issues from the most recent run log when none were given,
90
+ // mirroring runMergeChecks so `--watch` composes with auto-detection.
91
+ let issues = issueNumbers;
92
+ if (issues.length === 0) {
93
+ const runLog = findMostRecentLog(resolveLogDir());
94
+ if (runLog) {
95
+ issues = runLog.issues.map((i) => i.issueNumber);
96
+ }
97
+ }
98
+ const { targets, unresolved } = resolveWatchTargets(issues, repoRoot, gh);
99
+ if (targets.length === 0) {
100
+ const message = "Watch: no open PR found for " +
101
+ (issues.length > 0
102
+ ? `issue(s) ${issues.map((i) => `#${i}`).join(", ")}`
103
+ : "the most recent run") +
104
+ ". Push the branch and open a PR before running `merge --watch`.";
105
+ if (options.json) {
106
+ console.log(JSON.stringify({ watch: "blocked", error: message }, null, 2));
107
+ }
108
+ else {
109
+ console.error(ui.errorBox("Watch Blocked", message));
110
+ }
111
+ process.exitCode = 2;
112
+ return true;
113
+ }
114
+ // Validated, never NaN — the shared deadline below depends on it (a NaN
115
+ // deadline would make every per-PR `remaining` NaN and defeat --timeout).
116
+ const { intervalMs, timeoutMs } = resolveWatchTiming(options);
117
+ const deadline = Date.now() + timeoutMs;
118
+ if (unresolved.length > 0 && !options.json) {
119
+ console.log(colors.muted(` ! No PR resolved for: ${unresolved.map((i) => `#${i}`).join(", ")} — skipping in watch.`));
120
+ }
121
+ for (const { issue, pr } of targets) {
122
+ if (!options.json) {
123
+ console.log(colors.muted(`Watching PR #${pr} (issue #${issue})…`));
124
+ }
125
+ const remaining = deadline - Date.now();
126
+ const result = await waitForChecks(pr, {
127
+ intervalMs,
128
+ // Share the deadline across PRs so the whole gate honors --timeout.
129
+ timeoutMs: Math.max(0, remaining),
130
+ onPoll: options.json
131
+ ? undefined
132
+ : (msg) => console.log(colors.muted(` #${pr}: ${msg}`)),
133
+ }, gh);
134
+ if (result.status === "blocked") {
135
+ const reason = result.reason ?? "CI dispatch is blocked.";
136
+ if (options.json) {
137
+ console.log(JSON.stringify({
138
+ watch: "blocked",
139
+ pr,
140
+ issue,
141
+ reason,
142
+ checkName: result.checkName,
143
+ }, null, 2));
144
+ }
145
+ else {
146
+ console.log(ui.errorBox("BLOCKED", `PR #${pr} (issue #${issue}) — ${reason}` +
147
+ (result.checkName ? `\n\nCheck: ${result.checkName}` : "")));
148
+ }
149
+ process.exitCode = 2;
150
+ return true;
151
+ }
152
+ if (result.status === "timeout") {
153
+ const reason = result.reason ?? "Watch timed out.";
154
+ if (options.json) {
155
+ console.log(JSON.stringify({ watch: "timeout", pr, issue, reason }, null, 2));
156
+ }
157
+ else {
158
+ console.error(ui.errorBox("Watch Timed Out", reason));
159
+ }
160
+ process.exitCode = EXIT_WATCH_TIMEOUT;
161
+ return true;
162
+ }
163
+ // terminal → this PR is ready to be checked; continue to the next PR.
164
+ if (!options.json) {
165
+ console.log(colors.muted(` #${pr}: all checks terminal after ${result.polls} poll(s).`));
166
+ }
167
+ }
168
+ return false;
169
+ }
46
170
  /**
47
171
  * Main merge command handler
48
172
  */
@@ -51,7 +175,14 @@ export async function mergeCommand(issues, options) {
51
175
  if (!options.check && !options.scan && !options.review && !options.all) {
52
176
  options.check = true;
53
177
  }
54
- const repoRoot = getRepoRoot();
178
+ let repoRoot;
179
+ try {
180
+ repoRoot = getRepoRoot();
181
+ }
182
+ catch {
183
+ console.error(colors.error("❌ Not in a git repository. Run merge from inside your project repo."));
184
+ process.exit(1);
185
+ }
55
186
  const issueNumbers = issues
56
187
  .map((i) => parseInt(i, 10))
57
188
  .filter((n) => !isNaN(n));
@@ -81,6 +212,27 @@ export async function mergeCommand(issues, options) {
81
212
  }
82
213
  console.log("");
83
214
  }
215
+ // Watch gate (#818): wait for CI to reach a terminal state before running the
216
+ // report. Returns true when it has already handled the outcome (dispatch
217
+ // block, timeout, or no PR to watch) and set the exit code — stop here.
218
+ if (options.watch) {
219
+ try {
220
+ const handled = await runWatchGate(issueNumbers, options, repoRoot);
221
+ if (handled)
222
+ return;
223
+ }
224
+ catch (error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ if (options.json) {
227
+ console.log(JSON.stringify({ watch: "error", error: message }, null, 2));
228
+ }
229
+ else {
230
+ console.error(ui.errorBox("Watch Failed", message));
231
+ }
232
+ process.exitCode = 2;
233
+ return;
234
+ }
235
+ }
84
236
  try {
85
237
  const report = await runMergeChecks(issueNumbers, options, repoRoot);
86
238
  if (options.json) {
@@ -77,7 +77,12 @@ export class ReadySnapshotAdapter {
77
77
  this.status = "failed";
78
78
  break;
79
79
  }
80
- case "activity": {
80
+ // #804: an auto-wait surfaces here the same way activity does — as a
81
+ // refreshed nowLine on the still-running phase. Sharing one body is
82
+ // deliberate: a wait must NOT reach the complete/failed case, which
83
+ // would close out a phase that is merely paused.
84
+ case "activity":
85
+ case "waiting": {
81
86
  if (this.currentPhase) {
82
87
  this.currentPhase = {
83
88
  ...this.currentPhase,
@@ -14,7 +14,7 @@
14
14
  * human / no implementation), persists that state, and emits a structured gap
15
15
  * report. See `src/lib/workflow/ready-gate.ts` for the engine.
16
16
  */
17
- import { type ReadyPolicy } from "../lib/settings.js";
17
+ import { type ReadyPolicy, type SequantSettings } from "../lib/settings.js";
18
18
  import { type ReadyResult } from "../lib/workflow/ready-gate.js";
19
19
  export interface ReadyCommandOptions {
20
20
  policy?: string;
@@ -29,8 +29,11 @@ export interface ReadyCommandOptions {
29
29
  /**
30
30
  * Exit code from a ready result.
31
31
  * - 0: ready (awaiting human merge)
32
- * - 1: not ready — needs human intervention (budget/iterations/stagnation)
33
- * - 2: not ready no implementation (#534) or hard error
32
+ * - 1: not ready — needs human intervention (budget/iterations/stagnation,
33
+ * or a NO_VERDICT QA turn (#853): the implementation may exist, so this
34
+ * is a re-run case, not the "nothing there" exit)
35
+ * - 2: not ready — no implementation (#534), uncommitted-only work (#879), or
36
+ * hard error. Same class: nothing certifiable exists on the branch.
34
37
  */
35
38
  export declare function getReadyExitCode(result: ReadyResult): number;
36
39
  /**
@@ -40,6 +43,25 @@ export declare function getReadyExitCode(result: ReadyResult): number;
40
43
  * @internal Exported for testing only.
41
44
  */
42
45
  export declare function resolvePolicy(flag: string | undefined, settingsPolicy: ReadyPolicy): ReadyPolicy;
46
+ /**
47
+ * Resolve the numeric limits for a ready-gate run: CLI → settings → default.
48
+ *
49
+ * #833: these previously guarded only the CLI value (`typeof x === "number" &&
50
+ * x > 0`) and fell straight through to `settings.run.*` unchecked. But
51
+ * `settings.run.timeout` is user-authored JSON, and `ready`'s value does NOT
52
+ * pass through `buildExecutionConfig` — `ready-gate.ts`'s `buildPhaseConfig`
53
+ * assembles its own `ExecutionConfig`. So a `"timeout": 0` in settings.json
54
+ * reached `setTimeout` as a 0 ms delay and aborted every ready-gate phase on
55
+ * its first tick, with no warning: the #833 defect, on the path the original
56
+ * fix did not cover. Chaining `positiveOr` leaves no layer unchecked.
57
+ *
58
+ * @internal Exported for testing only.
59
+ */
60
+ export declare function resolveReadyLimits(options: Pick<ReadyCommandOptions, "maxIterations" | "budget" | "timeout">, settings: Pick<SequantSettings, "run">): {
61
+ maxIterations: number;
62
+ tokenBudget: number | undefined;
63
+ phaseTimeout: number;
64
+ };
43
65
  /**
44
66
  * Locate the worktree path for an issue from `git worktree list`.
45
67
  *
@@ -15,25 +15,32 @@
15
15
  * report. See `src/lib/workflow/ready-gate.ts` for the engine.
16
16
  */
17
17
  import { ui, colors } from "../lib/cli-ui.js";
18
- import { getSettings } from "../lib/settings.js";
18
+ import { getSettings, } from "../lib/settings.js";
19
19
  import { listWorktrees } from "../lib/workflow/worktree-manager.js";
20
20
  import { GitHubProvider } from "../lib/workflow/platforms/github.js";
21
21
  import { getStateManager } from "../lib/workflow/state-manager.js";
22
22
  import { executePhaseWithRetry } from "../lib/workflow/phase-executor.js";
23
23
  import { buildProgressWiring } from "./run-progress.js";
24
24
  import { ReadySnapshotAdapter } from "./ready-tui-adapter.js";
25
+ import { DEFAULT_CONFIG } from "../lib/workflow/types.js";
26
+ import { positiveOr } from "../lib/workflow/config-resolver.js";
25
27
  import { runReadyGate, parseNonGoals, } from "../lib/workflow/ready-gate.js";
26
28
  /**
27
29
  * Exit code from a ready result.
28
30
  * - 0: ready (awaiting human merge)
29
- * - 1: not ready — needs human intervention (budget/iterations/stagnation)
30
- * - 2: not ready no implementation (#534) or hard error
31
+ * - 1: not ready — needs human intervention (budget/iterations/stagnation,
32
+ * or a NO_VERDICT QA turn (#853): the implementation may exist, so this
33
+ * is a re-run case, not the "nothing there" exit)
34
+ * - 2: not ready — no implementation (#534), uncommitted-only work (#879), or
35
+ * hard error. Same class: nothing certifiable exists on the branch.
31
36
  */
32
37
  export function getReadyExitCode(result) {
33
38
  if (result.ready)
34
39
  return 0;
35
- if (result.reason === "NO_IMPLEMENTATION")
40
+ if (result.reason === "NO_IMPLEMENTATION" ||
41
+ result.reason === "UNCOMMITTED_ONLY") {
36
42
  return 2;
43
+ }
37
44
  return 1;
38
45
  }
39
46
  /**
@@ -47,6 +54,33 @@ export function resolvePolicy(flag, settingsPolicy) {
47
54
  return flag;
48
55
  return settingsPolicy;
49
56
  }
57
+ /**
58
+ * Resolve the numeric limits for a ready-gate run: CLI → settings → default.
59
+ *
60
+ * #833: these previously guarded only the CLI value (`typeof x === "number" &&
61
+ * x > 0`) and fell straight through to `settings.run.*` unchecked. But
62
+ * `settings.run.timeout` is user-authored JSON, and `ready`'s value does NOT
63
+ * pass through `buildExecutionConfig` — `ready-gate.ts`'s `buildPhaseConfig`
64
+ * assembles its own `ExecutionConfig`. So a `"timeout": 0` in settings.json
65
+ * reached `setTimeout` as a 0 ms delay and aborted every ready-gate phase on
66
+ * its first tick, with no warning: the #833 defect, on the path the original
67
+ * fix did not cover. Chaining `positiveOr` leaves no layer unchecked.
68
+ *
69
+ * @internal Exported for testing only.
70
+ */
71
+ export function resolveReadyLimits(options, settings) {
72
+ return {
73
+ maxIterations: positiveOr(options.maxIterations, positiveOr(settings.run.maxIterations, DEFAULT_CONFIG.maxIterations)),
74
+ // Budget is genuinely optional — `undefined` means "no budget", not "use a
75
+ // default" — so it keeps the two-state form rather than chaining.
76
+ tokenBudget: typeof options.budget === "number" &&
77
+ Number.isFinite(options.budget) &&
78
+ options.budget > 0
79
+ ? options.budget
80
+ : undefined,
81
+ phaseTimeout: positiveOr(options.timeout, positiveOr(settings.run.timeout, DEFAULT_CONFIG.phaseTimeout)),
82
+ };
83
+ }
50
84
  /**
51
85
  * Locate the worktree path for an issue from `git worktree list`.
52
86
  *
@@ -70,15 +104,7 @@ export async function readyCommand(issueArg, options) {
70
104
  }
71
105
  const settings = await getSettings();
72
106
  const policy = resolvePolicy(options.policy, settings.ready.policy);
73
- const maxIterations = typeof options.maxIterations === "number" && options.maxIterations > 0
74
- ? options.maxIterations
75
- : settings.run.maxIterations;
76
- const tokenBudget = typeof options.budget === "number" && options.budget > 0
77
- ? options.budget
78
- : undefined;
79
- const phaseTimeout = typeof options.timeout === "number" && options.timeout > 0
80
- ? options.timeout
81
- : settings.run.timeout;
107
+ const { maxIterations, tokenBudget, phaseTimeout } = resolveReadyLimits(options, settings);
82
108
  const mcp = options.mcp !== false;
83
109
  // Resolve the issue's existing worktree (reuses run/state worktree infra).
84
110
  const worktreePath = resolveWorktreePath(issueNumber);