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
@@ -190,13 +190,14 @@ Run relevant commands and capture their output for review.`,
190
190
  phaseRegistry.register({
191
191
  name: "qa",
192
192
  skill: "qa",
193
- promptTemplate: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow.",
193
+ promptTemplate: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow. Your turn is one-shot: you must emit a `### Verdict:` line before the turn ends — deferring to a later turn or a background CI poll is not available.",
194
194
  requiresWorktree: true,
195
195
  driverOverrides: {
196
196
  aider: {
197
197
  promptTemplate: `Review the changes for GitHub issue #{issue}.
198
198
  Run 'npm test' and 'npm run build' to verify everything works.
199
199
  Check each acceptance criterion from the issue comments.
200
+ Your turn is one-shot: emit a verdict now — do not defer to a later turn or a background CI poll. If the tree is unreviewable, emit AC_NOT_MET (code-caused red CI) or NEEDS_VERIFICATION (pending/infra-blocked CI); never end without a verdict.
200
201
  Output a verdict: READY_FOR_MERGE, AC_MET_BUT_NOT_A_PLUS, or AC_NOT_MET
201
202
  with format "### Verdict: <VERDICT>" followed by an explanation.`,
202
203
  },
@@ -9,6 +9,7 @@
9
9
  * Async interface methods delegate to the sync implementations.
10
10
  */
11
11
  import type { PlatformProvider, Issue, CreatePROptions, PRInfo, PRStatus, Comment } from "./platform-provider.js";
12
+ import type { AnnotatedCheck } from "../../qa/infra-blocked-ci.js";
12
13
  /**
13
14
  * PR merge status values (matches the casing returned by `gh pr view`).
14
15
  */
@@ -55,6 +56,30 @@ export interface BatchGitHubResult {
55
56
  pullRequests: Record<number, BatchPRInfo>;
56
57
  error?: string;
57
58
  }
59
+ /**
60
+ * A single entry in a PR's `statusCheckRollup`.
61
+ *
62
+ * `gh pr view N --json statusCheckRollup` returns a heterogeneous array of two
63
+ * shapes, discriminated by `__typename`:
64
+ * - `CheckRun` (GitHub Actions / Checks API): progress in `status`
65
+ * (QUEUED | IN_PROGRESS | COMPLETED) with the outcome in `conclusion`.
66
+ * - `StatusContext` (legacy commit statuses): combined state in `state`
67
+ * (EXPECTED | PENDING | SUCCESS | FAILURE | ERROR).
68
+ * Only the fields the watch loop consumes are typed; unknown fields are ignored.
69
+ */
70
+ export interface RollupEntry {
71
+ __typename?: string;
72
+ name?: string;
73
+ context?: string;
74
+ /** CheckRun progress. */
75
+ status?: string;
76
+ /** CheckRun outcome. */
77
+ conclusion?: string;
78
+ /** StatusContext combined state. */
79
+ state?: string;
80
+ }
81
+ /** PR mergeability as reported by `gh pr view --json mergeable`. */
82
+ export type MergeableState = "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
58
83
  export declare class GitHubProvider implements PlatformProvider {
59
84
  name: string;
60
85
  /**
@@ -151,6 +176,45 @@ export declare class GitHubProvider implements PlatformProvider {
151
176
  * Used by upstream/issues.ts.
152
177
  */
153
178
  commentOnIssueWithBodyFileSync(repo: string, issueNumber: number, bodyFile: string): boolean;
179
+ /**
180
+ * Fetch a PR's status-check rollup via `statusCheckRollup`.
181
+ *
182
+ * Uses `statusCheckRollup`, NOT the known-broken `--json checks` field
183
+ * (#443 / #818). Returns the raw heterogeneous entries; callers classify
184
+ * terminal-ness with the helpers in `merge-check/watch.ts`. Returns `[]` on
185
+ * any error so a transient `gh` failure degrades to "no checks yet" rather
186
+ * than crashing a poll loop.
187
+ */
188
+ getStatusCheckRollupSync(prNumber: number): RollupEntry[];
189
+ /**
190
+ * Fetch a PR's mergeability state via `--json mergeable`.
191
+ *
192
+ * Right after a push GitHub reports `UNKNOWN` while it recomputes, then
193
+ * settles to `MERGEABLE` or `CONFLICTING`. `merge --watch` treats
194
+ * `CONFLICTING` as a dispatch block: CI never starts against an unmergeable
195
+ * ref. Returns `UNKNOWN` on any error — the safe default that keeps the loop
196
+ * polling rather than falsely reporting a conflict.
197
+ */
198
+ getMergeableStateSync(prNumber: number): MergeableState;
199
+ /**
200
+ * Get a PR's head commit SHA via `--json headRefOid`.
201
+ *
202
+ * Needed to query the commit's check-run annotations for the billing-lockout
203
+ * signature. Returns `null` on error.
204
+ */
205
+ getPRHeadShaSync(prNumber: number): string | null;
206
+ /**
207
+ * Fetch check-run annotations for a commit, shaped for `detectInfraBlockedCi`.
208
+ *
209
+ * Lists the commit's check runs (per_page=100, no pagination needed for the
210
+ * billing-lockout case — every run carries the same annotation, so the first
211
+ * page is representative), then fetches annotations for each run that reports
212
+ * any. Used only when the whole board is failing, to recognise the "job was
213
+ * not started" signature (#820). Degrades to `[]` on any error (404, rate
214
+ * limit, revoked token) so a broken-CI situation never crashes the watch
215
+ * loop — `detectInfraBlockedCi` then reads that as "not infra-blocked".
216
+ */
217
+ getCheckRunAnnotationsSync(headSha: string): AnnotatedCheck[];
154
218
  fetchIssue(id: string): Promise<Issue>;
155
219
  postComment(issueId: string, body: string): Promise<void>;
156
220
  addLabel(issueId: string, label: string): Promise<void>;
@@ -414,6 +414,158 @@ export class GitHubProvider {
414
414
  return false;
415
415
  }
416
416
  }
417
+ // ─── Watch / CI-rollup helpers (for `merge --watch`, #818) ─────────
418
+ /**
419
+ * Fetch a PR's status-check rollup via `statusCheckRollup`.
420
+ *
421
+ * Uses `statusCheckRollup`, NOT the known-broken `--json checks` field
422
+ * (#443 / #818). Returns the raw heterogeneous entries; callers classify
423
+ * terminal-ness with the helpers in `merge-check/watch.ts`. Returns `[]` on
424
+ * any error so a transient `gh` failure degrades to "no checks yet" rather
425
+ * than crashing a poll loop.
426
+ */
427
+ getStatusCheckRollupSync(prNumber) {
428
+ try {
429
+ const result = spawnSync("gh", [
430
+ "pr",
431
+ "view",
432
+ String(prNumber),
433
+ "--json",
434
+ "statusCheckRollup",
435
+ "-q",
436
+ ".statusCheckRollup",
437
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
438
+ if (result.status !== 0 || !result.stdout)
439
+ return [];
440
+ const parsed = JSON.parse(result.stdout);
441
+ return Array.isArray(parsed) ? parsed : [];
442
+ }
443
+ catch {
444
+ return [];
445
+ }
446
+ }
447
+ /**
448
+ * Fetch a PR's mergeability state via `--json mergeable`.
449
+ *
450
+ * Right after a push GitHub reports `UNKNOWN` while it recomputes, then
451
+ * settles to `MERGEABLE` or `CONFLICTING`. `merge --watch` treats
452
+ * `CONFLICTING` as a dispatch block: CI never starts against an unmergeable
453
+ * ref. Returns `UNKNOWN` on any error — the safe default that keeps the loop
454
+ * polling rather than falsely reporting a conflict.
455
+ */
456
+ getMergeableStateSync(prNumber) {
457
+ try {
458
+ const result = spawnSync("gh", [
459
+ "pr",
460
+ "view",
461
+ String(prNumber),
462
+ "--json",
463
+ "mergeable",
464
+ "-q",
465
+ ".mergeable",
466
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
467
+ if (result.status === 0 && result.stdout) {
468
+ const state = result.stdout.toString().trim().toUpperCase();
469
+ if (state === "MERGEABLE" ||
470
+ state === "CONFLICTING" ||
471
+ state === "UNKNOWN") {
472
+ return state;
473
+ }
474
+ }
475
+ }
476
+ catch {
477
+ // fall through to UNKNOWN
478
+ }
479
+ return "UNKNOWN";
480
+ }
481
+ /**
482
+ * Get a PR's head commit SHA via `--json headRefOid`.
483
+ *
484
+ * Needed to query the commit's check-run annotations for the billing-lockout
485
+ * signature. Returns `null` on error.
486
+ */
487
+ getPRHeadShaSync(prNumber) {
488
+ try {
489
+ const result = spawnSync("gh", [
490
+ "pr",
491
+ "view",
492
+ String(prNumber),
493
+ "--json",
494
+ "headRefOid",
495
+ "-q",
496
+ ".headRefOid",
497
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
498
+ if (result.status === 0 && result.stdout) {
499
+ const sha = result.stdout.toString().trim();
500
+ return sha || null;
501
+ }
502
+ }
503
+ catch {
504
+ // fall through to null
505
+ }
506
+ return null;
507
+ }
508
+ /**
509
+ * Fetch check-run annotations for a commit, shaped for `detectInfraBlockedCi`.
510
+ *
511
+ * Lists the commit's check runs (per_page=100, no pagination needed for the
512
+ * billing-lockout case — every run carries the same annotation, so the first
513
+ * page is representative), then fetches annotations for each run that reports
514
+ * any. Used only when the whole board is failing, to recognise the "job was
515
+ * not started" signature (#820). Degrades to `[]` on any error (404, rate
516
+ * limit, revoked token) so a broken-CI situation never crashes the watch
517
+ * loop — `detectInfraBlockedCi` then reads that as "not infra-blocked".
518
+ */
519
+ getCheckRunAnnotationsSync(headSha) {
520
+ try {
521
+ const listResult = spawnSync("gh", [
522
+ "api",
523
+ `repos/{owner}/{repo}/commits/${headSha}/check-runs?per_page=100`,
524
+ "-q",
525
+ ".check_runs",
526
+ ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
527
+ if (listResult.status !== 0 || !listResult.stdout)
528
+ return [];
529
+ const runs = JSON.parse(listResult.stdout);
530
+ if (!Array.isArray(runs))
531
+ return [];
532
+ const annotated = [];
533
+ for (const run of runs) {
534
+ const checkName = typeof run?.name === "string" ? run.name : "unknown";
535
+ const id = run?.id;
536
+ if (typeof id !== "number" && typeof id !== "string")
537
+ continue;
538
+ // Skip runs that explicitly report zero annotations; keep fetching when
539
+ // the count is unknown, so a missing field never hides the signature.
540
+ const count = run?.output?.annotations_count;
541
+ if (typeof count === "number" && count === 0)
542
+ continue;
543
+ const annResult = spawnSync("gh", ["api", `repos/{owner}/{repo}/check-runs/${id}/annotations`], {
544
+ encoding: "utf-8",
545
+ stdio: ["pipe", "pipe", "pipe"],
546
+ timeout: 15000,
547
+ });
548
+ if (annResult.status !== 0 || !annResult.stdout) {
549
+ annotated.push({ checkName, annotations: [] });
550
+ continue;
551
+ }
552
+ try {
553
+ const annotations = JSON.parse(annResult.stdout);
554
+ annotated.push({
555
+ checkName,
556
+ annotations: Array.isArray(annotations) ? annotations : [],
557
+ });
558
+ }
559
+ catch {
560
+ annotated.push({ checkName, annotations: [] });
561
+ }
562
+ }
563
+ return annotated;
564
+ }
565
+ catch {
566
+ return [];
567
+ }
568
+ }
417
569
  // ─── Async interface methods (PlatformProvider) ────────────────────
418
570
  async fetchIssue(id) {
419
571
  const result = spawnSync("gh", ["issue", "view", id, "--json", "number,title,body,labels,state"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
@@ -130,6 +130,8 @@ export interface QACacheOptions {
130
130
  defaultTtl?: number;
131
131
  /** Enable verbose logging */
132
132
  verbose?: boolean;
133
+ /** Repository directory for git operations (default: process.cwd()) */
134
+ cwd?: string;
133
135
  }
134
136
  /**
135
137
  * Result of cache lookup
@@ -153,6 +155,7 @@ export declare class QACache {
153
155
  private cacheDir;
154
156
  private defaultTtl;
155
157
  private verbose;
158
+ private cwd;
156
159
  private cachedState;
157
160
  constructor(options?: QACacheOptions);
158
161
  /**
@@ -160,9 +163,11 @@ export declare class QACache {
160
163
  */
161
164
  getCachePath(): string;
162
165
  /**
163
- * Compute hash of git diff between main and HEAD
166
+ * Compute hash of git diff between the resolved base and HEAD
164
167
  *
165
168
  * This provides a content-based cache key that changes when code changes.
169
+ * The base is resolved via resolveDiffBase so a stale local main does not
170
+ * leak merged-upstream content into the cache key (#890).
166
171
  */
167
172
  computeDiffHash(): string;
168
173
  /**
@@ -27,8 +27,9 @@
27
27
  import * as fs from "fs";
28
28
  import * as path from "path";
29
29
  import * as crypto from "crypto";
30
- import { execSync } from "child_process";
30
+ import { execFileSync } from "child_process";
31
31
  import { z } from "zod";
32
+ import { resolveDiffBase } from "./git-diff-utils.js";
32
33
  /**
33
34
  * Check types that can be cached
34
35
  */
@@ -74,7 +75,7 @@ export const CachedCheckResultSchema = z.object({
74
75
  export const QARunContextSchema = z.object({
75
76
  /** Git HEAD SHA at the time of QA completion */
76
77
  lastQACommitSHA: z.string(),
77
- /** Diff hash (main...HEAD) at the time of QA */
78
+ /** Diff hash (resolved base...HEAD) at the time of QA */
78
79
  lastQADiffHash: z.string(),
79
80
  /** Map of AC ID → status from the last QA run */
80
81
  acStatuses: z.record(z.string(), z.enum(["met", "not_met", "partially_met", "pending", "blocked"])),
@@ -139,11 +140,13 @@ export class QACache {
139
140
  cacheDir;
140
141
  defaultTtl;
141
142
  verbose;
143
+ cwd;
142
144
  cachedState = null;
143
145
  constructor(options = {}) {
144
146
  this.cacheDir = options.cacheDir ?? DEFAULT_CACHE_DIR;
145
147
  this.defaultTtl = options.defaultTtl ?? DEFAULT_TTL;
146
148
  this.verbose = options.verbose ?? false;
149
+ this.cwd = options.cwd ?? process.cwd();
147
150
  }
148
151
  /**
149
152
  * Get the cache file path
@@ -152,14 +155,18 @@ export class QACache {
152
155
  return path.join(this.cacheDir, "cache.json");
153
156
  }
154
157
  /**
155
- * Compute hash of git diff between main and HEAD
158
+ * Compute hash of git diff between the resolved base and HEAD
156
159
  *
157
160
  * This provides a content-based cache key that changes when code changes.
161
+ * The base is resolved via resolveDiffBase so a stale local main does not
162
+ * leak merged-upstream content into the cache key (#890).
158
163
  */
159
164
  computeDiffHash() {
160
165
  try {
161
- // Get the diff content between main and HEAD
162
- const diff = execSync("git diff main...HEAD", {
166
+ // Get the diff content between the resolved base and HEAD
167
+ const base = resolveDiffBase(this.cwd, "main");
168
+ const diff = execFileSync("git", ["diff", `${base}...HEAD`], {
169
+ cwd: this.cwd,
163
170
  encoding: "utf-8",
164
171
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer
165
172
  });
@@ -236,7 +243,9 @@ export class QACache {
236
243
  */
237
244
  async checkGlobalInvalidation() {
238
245
  try {
239
- const changedFiles = execSync("git diff main...HEAD --name-only", {
246
+ const base = resolveDiffBase(this.cwd, "main");
247
+ const changedFiles = execFileSync("git", ["diff", `${base}...HEAD`, "--name-only"], {
248
+ cwd: this.cwd,
240
249
  encoding: "utf-8",
241
250
  })
242
251
  .trim()
@@ -263,7 +272,9 @@ export class QACache {
263
272
  return false;
264
273
  }
265
274
  try {
266
- const changedFiles = execSync("git diff main...HEAD --name-only", {
275
+ const base = resolveDiffBase(this.cwd, "main");
276
+ const changedFiles = execFileSync("git", ["diff", `${base}...HEAD`, "--name-only"], {
277
+ cwd: this.cwd,
267
278
  encoding: "utf-8",
268
279
  })
269
280
  .trim()
@@ -432,7 +443,8 @@ export class QACache {
432
443
  return null;
433
444
  }
434
445
  try {
435
- const diff = execSync(`git diff ${sinceCommit}...HEAD`, {
446
+ const diff = execFileSync("git", ["diff", `${sinceCommit}...HEAD`], {
447
+ cwd: this.cwd,
436
448
  encoding: "utf-8",
437
449
  maxBuffer: 10 * 1024 * 1024,
438
450
  });
@@ -460,7 +472,8 @@ export class QACache {
460
472
  return null;
461
473
  }
462
474
  try {
463
- const output = execSync(`git diff ${sinceCommit}...HEAD --name-only`, {
475
+ const output = execFileSync("git", ["diff", `${sinceCommit}...HEAD`, "--name-only"], {
476
+ cwd: this.cwd,
464
477
  encoding: "utf-8",
465
478
  });
466
479
  return output.trim().split("\n").filter(Boolean);
@@ -26,6 +26,7 @@ import type { QaVerdict } from "./run-log-schema.js";
26
26
  import type { ReadyPolicy } from "../settings.js";
27
27
  import type { IssueStatus } from "./state-schema.js";
28
28
  import { type LoopProgressSnapshot } from "./qa-stagnation.js";
29
+ import { type ExecChangeState } from "./phase-executor.js";
29
30
  export type { ReadyPolicy } from "../settings.js";
30
31
  /**
31
32
  * Why the gate stopped. Drives `ready`, the persisted issue status, and the
@@ -44,8 +45,20 @@ export type ReadyTerminalReason =
44
45
  | "LOOP_NO_DIFF"
45
46
  /** Guard: `/loop` phase itself failed. Needs human. */
46
47
  | "LOOP_FAILED"
47
- /** #534: zero-diff exec or null/unparseable QA verdict. Not ready. */
48
- | "NO_IMPLEMENTATION";
48
+ /** #534: zero-diff exec worktree nothing was built. Not ready. */
49
+ | "NO_IMPLEMENTATION"
50
+ /**
51
+ * #879: the worktree is dirty but has no commits. Distinct from
52
+ * NO_IMPLEMENTATION — work exists, but uncommitted work cannot rebase, push,
53
+ * or become a PR, so it is not certifiable as-is. Re-run once it is committed.
54
+ */
55
+ | "UNCOMMITTED_ONLY"
56
+ /**
57
+ * #853: QA ran but produced no verdict (deferred its one-shot turn, or
58
+ * output was unparseable). Distinct from NO_IMPLEMENTATION — the
59
+ * implementation may be complete; it is the *review* that is missing.
60
+ */
61
+ | "NO_VERDICT";
49
62
  /** A single gap surfaced by QA, classified for the report. */
50
63
  export interface ReadyGapItem {
51
64
  /** Gap description as surfaced by QA. */
@@ -110,8 +123,8 @@ export interface RunReadyGateOptions {
110
123
  onProgress?: ProgressCallback;
111
124
  /** Injectable token reader — defaults to reading `<worktree>/.sequant`. */
112
125
  readTokensUsed?: (worktreePath: string) => number;
113
- /** Injectable change detector — defaults to {@link hasExecChanges}. */
114
- hasChangesFn?: (cwd: string) => boolean;
126
+ /** Injectable change classifier — defaults to {@link classifyExecChanges}. */
127
+ classifyChangesFn?: (cwd: string) => ExecChangeState;
115
128
  /** Injectable loop-progress snapshot — defaults to {@link snapshotLoopProgress}. */
116
129
  snapshotFn?: (cwd: string) => LoopProgressSnapshot;
117
130
  }
@@ -22,7 +22,7 @@
22
22
  * lives in `src/commands/ready.ts`.
23
23
  */
24
24
  import { snapshotLoopProgress, compareLoopProgress, } from "./qa-stagnation.js";
25
- import { hasExecChanges } from "./phase-executor.js";
25
+ import { classifyExecChanges } from "./phase-executor.js";
26
26
  import { readTokenUsageFiles, aggregateTokenUsage, TOKEN_USAGE_DIR, } from "./token-utils.js";
27
27
  import * as path from "path";
28
28
  /**
@@ -180,7 +180,11 @@ export function formatReadyReport(result) {
180
180
  ? "✅ READY — awaiting human merge decision"
181
181
  : result.reason === "NO_IMPLEMENTATION"
182
182
  ? "⛔ NOT READY — no implementation detected"
183
- : "⚠️ NOT READY — needs human intervention";
183
+ : result.reason === "UNCOMMITTED_ONLY"
184
+ ? "⛔ NOT READY — work is uncommitted"
185
+ : result.reason === "NO_VERDICT"
186
+ ? "⛔ NOT READY — QA produced no verdict"
187
+ : "⚠️ NOT READY — needs human intervention";
184
188
  const reasonText = {
185
189
  AC_MET: "Acceptance Criteria are objectively met (no AC_NOT_MET). Remaining gaps are quality-only and reported below (policy `ac` does not auto-fix them).",
186
190
  READY_FOR_MERGE: "QA returned READY_FOR_MERGE.",
@@ -188,7 +192,9 @@ export function formatReadyReport(result) {
188
192
  TOKEN_BUDGET: "Token budget exhausted before reaching the policy threshold. A human should review the remaining gaps.",
189
193
  LOOP_NO_DIFF: "The fix loop made no diff (stagnation guard). Manual intervention required.",
190
194
  LOOP_FAILED: "The fix loop phase failed. A human should investigate before merging.",
191
- NO_IMPLEMENTATION: "Zero-diff worktree or null/unparseable QA verdict — there is nothing to certify (#534 guard).",
195
+ NO_IMPLEMENTATION: "Zero-diff worktree — there is nothing to certify (#534 guard).",
196
+ UNCOMMITTED_ONLY: "The worktree has uncommitted changes but no commits (#879 guard). Uncommitted work cannot rebase, push, or become a PR — commit it, then re-run the gate.",
197
+ NO_VERDICT: "QA ran but produced no verdict (deferred one-shot turn or unparseable output) — there is nothing to certify (#534/#853 guard). The implementation may exist; re-run the gate once QA emits a verdict.",
192
198
  };
193
199
  const lines = [];
194
200
  lines.push(`## sequant ready — Issue #${result.issueNumber}`);
@@ -231,7 +237,7 @@ export function formatReadyReport(result) {
231
237
  export async function runReadyGate(opts) {
232
238
  const { issueNumber, worktreePath, policy, maxIterations, tokenBudget, nonGoals = [], } = opts;
233
239
  const readTokensUsed = opts.readTokensUsed ?? defaultReadTokensUsed;
234
- const hasChangesFn = opts.hasChangesFn ?? hasExecChanges;
240
+ const classifyChangesFn = opts.classifyChangesFn ?? classifyExecChanges;
235
241
  const snapshotFn = opts.snapshotFn ?? snapshotLoopProgress;
236
242
  // #697: run a phase while emitting live-progress events around it. `iteration`
237
243
  // is the 1-based QA-pass index so the renderer can render `loop N/M`. Emits
@@ -296,15 +302,25 @@ export async function runReadyGate(opts) {
296
302
  const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
297
303
  tokensUsed = readTokensUsed(worktreePath);
298
304
  const verdict = qaResult.verdict ?? null;
299
- // #534 guard: a null/unparseable verdict is never "ready".
305
+ // #534 guard: a null verdict is never "ready". #853: report it as
306
+ // NO_VERDICT, not NO_IMPLEMENTATION — a deferred/unparseable QA turn says
307
+ // nothing about whether an implementation exists, and the misleading label
308
+ // sent debuggers to the wrong place (same class as the phase-executor
309
+ // "unparseable verdict" split).
300
310
  if (!verdict) {
301
- return finish("NO_IMPLEMENTATION");
311
+ return finish("NO_VERDICT");
302
312
  }
303
- // #534 guard: an empty worktree (no commits, no uncommitted work) is never
304
- // "ready" replays the #529/#570 empty-branch class.
305
- if (!hasChangesFn(worktreePath)) {
313
+ // #534/#879 guard: a worktree with no commits is never "ready". Split by
314
+ // cause so the report is not misleading: `none` is the empty-branch class
315
+ // (#529/#570), `uncommitted` is a dirty tree whose work cannot become a PR
316
+ // until committed (#879). `commits`/`unknown` continue (unknown fails open).
317
+ const changes = classifyChangesFn(worktreePath);
318
+ if (changes.kind === "none") {
306
319
  return finish("NO_IMPLEMENTATION");
307
320
  }
321
+ if (changes.kind === "uncommitted") {
322
+ return finish("UNCOMMITTED_ONLY");
323
+ }
308
324
  finalVerdict = verdict;
309
325
  const gaps = qaResult.summary?.gaps ?? [];
310
326
  remaining = classifyGaps(gaps, nonGoals);
@@ -89,6 +89,8 @@ export declare const ErrorContextSchema: z.ZodObject<{
89
89
  api_error: "api_error";
90
90
  hook_failure: "hook_failure";
91
91
  build_error: "build_error";
92
+ rate_limit: "rate_limit";
93
+ billing: "billing";
92
94
  }>;
93
95
  errorType: z.ZodOptional<z.ZodString>;
94
96
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -170,6 +172,8 @@ export declare const PhaseLogSchema: z.ZodObject<{
170
172
  api_error: "api_error";
171
173
  hook_failure: "hook_failure";
172
174
  build_error: "build_error";
175
+ rate_limit: "rate_limit";
176
+ billing: "billing";
173
177
  }>;
174
178
  errorType: z.ZodOptional<z.ZodString>;
175
179
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -247,6 +251,8 @@ export declare const IssueLogSchema: z.ZodObject<{
247
251
  api_error: "api_error";
248
252
  hook_failure: "hook_failure";
249
253
  build_error: "build_error";
254
+ rate_limit: "rate_limit";
255
+ billing: "billing";
250
256
  }>;
251
257
  errorType: z.ZodOptional<z.ZodString>;
252
258
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -254,6 +260,8 @@ export declare const IssueLogSchema: z.ZodObject<{
254
260
  }, z.core.$strip>>;
255
261
  }, z.core.$strip>>;
256
262
  totalDurationSeconds: z.ZodNumber;
263
+ aborted: z.ZodOptional<z.ZodBoolean>;
264
+ abortReason: z.ZodOptional<z.ZodString>;
257
265
  prNumber: z.ZodOptional<z.ZodNumber>;
258
266
  prUrl: z.ZodOptional<z.ZodString>;
259
267
  }, z.core.$strip>;
@@ -277,6 +285,8 @@ export declare const RunSummarySchema: z.ZodObject<{
277
285
  totalIssues: z.ZodNumber;
278
286
  passed: z.ZodNumber;
279
287
  failed: z.ZodNumber;
288
+ partial: z.ZodDefault<z.ZodNumber>;
289
+ aborted: z.ZodDefault<z.ZodNumber>;
280
290
  totalDurationSeconds: z.ZodNumber;
281
291
  }, z.core.$strip>;
282
292
  export type RunSummary = z.infer<typeof RunSummarySchema>;
@@ -365,6 +375,8 @@ export declare const RunLogSchema: z.ZodObject<{
365
375
  api_error: "api_error";
366
376
  hook_failure: "hook_failure";
367
377
  build_error: "build_error";
378
+ rate_limit: "rate_limit";
379
+ billing: "billing";
368
380
  }>;
369
381
  errorType: z.ZodOptional<z.ZodString>;
370
382
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -372,6 +384,8 @@ export declare const RunLogSchema: z.ZodObject<{
372
384
  }, z.core.$strip>>;
373
385
  }, z.core.$strip>>;
374
386
  totalDurationSeconds: z.ZodNumber;
387
+ aborted: z.ZodOptional<z.ZodBoolean>;
388
+ abortReason: z.ZodOptional<z.ZodString>;
375
389
  prNumber: z.ZodOptional<z.ZodNumber>;
376
390
  prUrl: z.ZodOptional<z.ZodString>;
377
391
  }, z.core.$strip>>;
@@ -379,10 +393,13 @@ export declare const RunLogSchema: z.ZodObject<{
379
393
  totalIssues: z.ZodNumber;
380
394
  passed: z.ZodNumber;
381
395
  failed: z.ZodNumber;
396
+ partial: z.ZodDefault<z.ZodNumber>;
397
+ aborted: z.ZodDefault<z.ZodNumber>;
382
398
  totalDurationSeconds: z.ZodNumber;
383
399
  }, z.core.$strip>;
384
400
  startCommit: z.ZodOptional<z.ZodString>;
385
401
  endCommit: z.ZodOptional<z.ZodString>;
402
+ abortedBy: z.ZodOptional<z.ZodString>;
386
403
  }, z.core.$strip>;
387
404
  export type RunLog = z.infer<typeof RunLogSchema>;
388
405
  /**
@@ -411,6 +428,7 @@ export declare function generateLogFilename(runId: string, startTime: Date): str
411
428
  */
412
429
  export declare function createEmptyRunLog(config: RunConfig, options?: {
413
430
  startCommit?: string;
431
+ startTime?: Date;
414
432
  }): Omit<RunLog, "endTime">;
415
433
  /**
416
434
  * Create a phase log entry
@@ -438,4 +456,23 @@ export declare function completePhaseLog(phaseLog: Omit<PhaseLog, "endTime" | "d
438
456
  */
439
457
  export declare function finalizeRunLog(runLog: Omit<RunLog, "endTime">, options?: {
440
458
  endCommit?: string;
459
+ abortedBy?: string;
441
460
  }): RunLog;
461
+ /**
462
+ * Derive an issue's overall log status from its phase log entries (#766).
463
+ *
464
+ * Phases are appended in execution order, so the last entry for a given phase
465
+ * name is its latest attempt — that attempt wins. This lets a `timeout`/
466
+ * `failure` that a later quality-loop iteration recovers from de-escalate to
467
+ * `success`, keeping the JSON log consistent with the live card and summary
468
+ * table (AC-3/AC-5). `loop` is auxiliary recovery and never determines the
469
+ * verdict (mirrors the live-card rule); an unrecovered failure still leaves a
470
+ * non-loop phase failed. Priority among latest attempts: failure > timeout >
471
+ * success.
472
+ *
473
+ * An issue with no phase entries is a `failure`, not a `success` (#856). The
474
+ * only way to reach that state is for `startIssue` to have run while no phase
475
+ * ever completed — i.e. the run was cut short. Returning `success` there is
476
+ * what let a SIGTERM'd run persist as a pass with an empty `phases[]`.
477
+ */
478
+ export declare function deriveIssueLogStatus(phases: PhaseLog[]): IssueStatus;