sequant 2.9.0 → 2.11.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +20 -5
  4. package/dist/bin/cli.js +143 -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 +56 -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 +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  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 +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  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 +3 -1
  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/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.d.ts +20 -1
  36. package/dist/src/commands/locks.js +208 -4
  37. package/dist/src/commands/logs.js +14 -5
  38. package/dist/src/commands/merge.js +154 -2
  39. package/dist/src/commands/ready-tui-adapter.js +6 -1
  40. package/dist/src/commands/ready.d.ts +31 -3
  41. package/dist/src/commands/ready.js +53 -13
  42. package/dist/src/commands/resume.d.ts +113 -0
  43. package/dist/src/commands/resume.js +193 -0
  44. package/dist/src/commands/run-display.js +38 -4
  45. package/dist/src/commands/run-flags.d.ts +42 -1
  46. package/dist/src/commands/run-flags.js +53 -1
  47. package/dist/src/commands/run-progress.js +26 -1
  48. package/dist/src/commands/run.js +11 -10
  49. package/dist/src/commands/state.js +7 -0
  50. package/dist/src/commands/status.d.ts +9 -0
  51. package/dist/src/commands/status.js +24 -1
  52. package/dist/src/commands/sync.js +90 -19
  53. package/dist/src/commands/update.js +28 -5
  54. package/dist/src/commands/worktree.d.ts +31 -0
  55. package/dist/src/commands/worktree.js +95 -0
  56. package/dist/src/lib/ac-parser.d.ts +14 -0
  57. package/dist/src/lib/ac-parser.js +99 -5
  58. package/dist/src/lib/assess/index.d.ts +10 -0
  59. package/dist/src/lib/assess/index.js +9 -0
  60. package/dist/src/lib/assess/renderer.d.ts +64 -0
  61. package/dist/src/lib/assess/renderer.js +481 -0
  62. package/dist/src/lib/assess/types.d.ts +224 -0
  63. package/dist/src/lib/assess/types.js +241 -0
  64. package/dist/src/lib/ci/outputs.d.ts +15 -2
  65. package/dist/src/lib/ci/outputs.js +17 -5
  66. package/dist/src/lib/cli-flags.d.ts +56 -0
  67. package/dist/src/lib/cli-flags.js +89 -0
  68. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
  69. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  70. package/dist/src/lib/cli-ui/run-renderer.js +69 -1
  71. package/dist/src/lib/errors.d.ts +78 -6
  72. package/dist/src/lib/errors.js +133 -15
  73. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  74. package/dist/src/lib/locks/checkout-lock.js +389 -0
  75. package/dist/src/lib/locks/index.d.ts +7 -4
  76. package/dist/src/lib/locks/index.js +5 -3
  77. package/dist/src/lib/locks/lock-manager.d.ts +101 -3
  78. package/dist/src/lib/locks/lock-manager.js +277 -8
  79. package/dist/src/lib/locks/types.d.ts +106 -3
  80. package/dist/src/lib/locks/types.js +44 -0
  81. package/dist/src/lib/mcp-config.d.ts +45 -0
  82. package/dist/src/lib/mcp-config.js +77 -1
  83. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  84. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  85. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  86. package/dist/src/lib/merge-check/command-result.js +56 -0
  87. package/dist/src/lib/merge-check/index.d.ts +4 -0
  88. package/dist/src/lib/merge-check/index.js +1 -1
  89. package/dist/src/lib/merge-check/report.js +5 -1
  90. package/dist/src/lib/merge-check/types.d.ts +9 -0
  91. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  92. package/dist/src/lib/merge-check/watch.js +215 -0
  93. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  94. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  95. package/dist/src/lib/settings.d.ts +87 -0
  96. package/dist/src/lib/settings.js +106 -8
  97. package/dist/src/lib/shutdown.d.ts +42 -2
  98. package/dist/src/lib/shutdown.js +75 -5
  99. package/dist/src/lib/skills-check.d.ts +26 -0
  100. package/dist/src/lib/skills-check.js +31 -0
  101. package/dist/src/lib/stacks.d.ts +110 -1
  102. package/dist/src/lib/stacks.js +244 -5
  103. package/dist/src/lib/templates.d.ts +74 -0
  104. package/dist/src/lib/templates.js +171 -16
  105. package/dist/src/lib/test-tautology-detector.d.ts +10 -2
  106. package/dist/src/lib/test-tautology-detector.js +213 -12
  107. package/dist/src/lib/version-check.d.ts +31 -0
  108. package/dist/src/lib/version-check.js +45 -2
  109. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  110. package/dist/src/lib/workflow/batch-executor.js +552 -60
  111. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  112. package/dist/src/lib/workflow/chain-resume.js +24 -20
  113. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  114. package/dist/src/lib/workflow/completed-status.js +57 -0
  115. package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
  116. package/dist/src/lib/workflow/config-resolver.js +143 -2
  117. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  118. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  119. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  120. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  121. package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
  122. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  123. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  124. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  125. package/dist/src/lib/workflow/error-classifier.js +4 -0
  126. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  127. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  128. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  129. package/dist/src/lib/workflow/heartbeat.js +90 -0
  130. package/dist/src/lib/workflow/log-writer.d.ts +54 -2
  131. package/dist/src/lib/workflow/log-writer.js +95 -6
  132. package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
  133. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  134. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  135. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  136. package/dist/src/lib/workflow/phase-detection.js +5 -1
  137. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  138. package/dist/src/lib/workflow/phase-executor.js +467 -52
  139. package/dist/src/lib/workflow/phase-registry.js +2 -1
  140. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  141. package/dist/src/lib/workflow/platforms/github.js +152 -0
  142. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  143. package/dist/src/lib/workflow/qa-cache.js +22 -9
  144. package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
  145. package/dist/src/lib/workflow/ready-gate.js +49 -12
  146. package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
  147. package/dist/src/lib/workflow/run-log-schema.js +78 -2
  148. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  149. package/dist/src/lib/workflow/run-orchestrator.js +168 -20
  150. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  151. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  152. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  153. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  154. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  155. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  156. package/dist/src/lib/workflow/state-manager.js +83 -0
  157. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  158. package/dist/src/lib/workflow/state-schema.js +47 -0
  159. package/dist/src/lib/workflow/types.d.ts +190 -4
  160. package/dist/src/lib/workflow/types.js +1 -0
  161. package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
  162. package/dist/src/lib/workflow/worktree-manager.js +111 -24
  163. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  164. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  165. package/dist/src/mcp/tools/run.d.ts +9 -1
  166. package/dist/src/mcp/tools/run.js +20 -1
  167. package/dist/src/mcp/tools/status.js +6 -0
  168. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  169. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  170. package/dist/src/ui/tui/IssueBox.js +10 -2
  171. package/dist/src/ui/tui/row-cap.js +2 -1
  172. package/dist/src/ui/tui/theme.d.ts +7 -0
  173. package/dist/src/ui/tui/theme.js +9 -0
  174. package/package.json +10 -8
  175. package/templates/hooks/parallel-marker.sh +50 -0
  176. package/templates/hooks/post-tool.sh +20 -7
  177. package/templates/hooks/pre-tool.sh +303 -10
  178. package/templates/scripts/cleanup-worktree.sh +217 -33
  179. package/templates/scripts/new-feature.sh +289 -35
  180. package/templates/skills/_shared/references/subagent-types.md +7 -18
  181. package/templates/skills/_shared/references/trust-model.md +18 -0
  182. package/templates/skills/assess/SKILL.md +220 -391
  183. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  184. package/templates/skills/docs/SKILL.md +4 -4
  185. package/templates/skills/exec/SKILL.md +87 -13
  186. package/templates/skills/fullsolve/SKILL.md +130 -22
  187. package/templates/skills/loop/SKILL.md +94 -13
  188. package/templates/skills/merger/SKILL.md +100 -12
  189. package/templates/skills/qa/SKILL.md +396 -63
  190. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  191. package/templates/skills/qa/references/call-site-review.md +2 -2
  192. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  193. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  194. package/templates/skills/qa/references/quality-gates.md +21 -7
  195. package/templates/skills/qa/references/testing-requirements.md +1 -1
  196. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  197. package/templates/skills/release/SKILL.md +80 -1
  198. package/templates/skills/setup/SKILL.md +5 -5
  199. package/templates/skills/spec/SKILL.md +33 -15
  200. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  201. package/templates/skills/test/SKILL.md +1 -1
  202. package/templates/skills/testgen/SKILL.md +23 -6
  203. package/dist/src/lib/phase-spinner.d.ts +0 -146
  204. package/dist/src/lib/phase-spinner.js +0 -255
  205. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  206. package/dist/src/lib/workflow/pr-operations.js +0 -326
  207. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  208. package/dist/src/lib/workflow/run-summary.js +0 -142
  209. package/templates/agents/sequant-explorer.md +0 -24
@@ -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);
@@ -22,10 +22,12 @@
22
22
  * lives in `src/commands/ready.ts`.
23
23
  */
24
24
  import type { ExecutionConfig, PhaseResult, ProgressCallback } from "./types.js";
25
+ import { type EscalationRecord } from "./effort-escalation.js";
25
26
  import type { QaVerdict } from "./run-log-schema.js";
26
27
  import type { ReadyPolicy } from "../settings.js";
27
28
  import type { IssueStatus } from "./state-schema.js";
28
29
  import { type LoopProgressSnapshot } from "./qa-stagnation.js";
30
+ import { type ExecChangeState } from "./phase-executor.js";
29
31
  export type { ReadyPolicy } from "../settings.js";
30
32
  /**
31
33
  * Why the gate stopped. Drives `ready`, the persisted issue status, and the
@@ -44,8 +46,20 @@ export type ReadyTerminalReason =
44
46
  | "LOOP_NO_DIFF"
45
47
  /** Guard: `/loop` phase itself failed. Needs human. */
46
48
  | "LOOP_FAILED"
47
- /** #534: zero-diff exec or null/unparseable QA verdict. Not ready. */
48
- | "NO_IMPLEMENTATION";
49
+ /** #534: zero-diff exec worktree nothing was built. Not ready. */
50
+ | "NO_IMPLEMENTATION"
51
+ /**
52
+ * #879: the worktree is dirty but has no commits. Distinct from
53
+ * NO_IMPLEMENTATION — work exists, but uncommitted work cannot rebase, push,
54
+ * or become a PR, so it is not certifiable as-is. Re-run once it is committed.
55
+ */
56
+ | "UNCOMMITTED_ONLY"
57
+ /**
58
+ * #853: QA ran but produced no verdict (deferred its one-shot turn, or
59
+ * output was unparseable). Distinct from NO_IMPLEMENTATION — the
60
+ * implementation may be complete; it is the *review* that is missing.
61
+ */
62
+ | "NO_VERDICT";
49
63
  /** A single gap surfaced by QA, classified for the report. */
50
64
  export interface ReadyGapItem {
51
65
  /** Gap description as surfaced by QA. */
@@ -77,6 +91,12 @@ export interface ReadyResult {
77
91
  tokensUsed: number;
78
92
  /** Human-readable markdown gap report (AC-4). */
79
93
  report: string;
94
+ /**
95
+ * Effort escalations applied during this gate's QA-pass loop (#915),
96
+ * base+escalated tier per escalated `qa`/`loop` dispatch. Empty when
97
+ * `effortEscalation` is off or no dispatch escalated.
98
+ */
99
+ effortEscalations: EscalationRecord[];
80
100
  }
81
101
  /**
82
102
  * Thin phase-runner abstraction so the engine can be unit-tested without the
@@ -110,10 +130,31 @@ export interface RunReadyGateOptions {
110
130
  onProgress?: ProgressCallback;
111
131
  /** Injectable token reader — defaults to reading `<worktree>/.sequant`. */
112
132
  readTokensUsed?: (worktreePath: string) => number;
113
- /** Injectable change detector — defaults to {@link hasExecChanges}. */
114
- hasChangesFn?: (cwd: string) => boolean;
133
+ /** Injectable change classifier — defaults to {@link classifyExecChanges}. */
134
+ classifyChangesFn?: (cwd: string) => ExecChangeState;
115
135
  /** Injectable loop-progress snapshot — defaults to {@link snapshotLoopProgress}. */
116
136
  snapshotFn?: (cwd: string) => LoopProgressSnapshot;
137
+ /**
138
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
139
+ * name. Callers (e.g. `commands/ready.ts`) resolve this via
140
+ * `resolvePhasePolicies` — the same shared resolver `buildExecutionConfig`
141
+ * uses — so this producer cannot drift from that one (#833 class).
142
+ * `buildPhaseConfig` spreads it onto every `ExecutionConfig` it builds;
143
+ * `phase-executor.ts` applies the entry for the phase actually running.
144
+ */
145
+ phasePolicies?: Record<string, {
146
+ model?: string;
147
+ effort?: string;
148
+ }>;
149
+ /**
150
+ * Evidence-based effort escalation on quality-loop retries (#915). Callers
151
+ * (e.g. `commands/ready.ts`) resolve this CLI > settings > `false`, the same
152
+ * precedence `buildExecutionConfig` uses for the `run` path (#833 class).
153
+ * `buildPhaseConfig` spreads it onto every `ExecutionConfig` it builds;
154
+ * `withEscalatedEffort` (`effort-escalation.ts`) reads it at each QA-pass
155
+ * dispatch to decide whether that specific `qa`/`loop` call escalates.
156
+ */
157
+ effortEscalation?: boolean;
117
158
  }
118
159
  /**
119
160
  * Pure exit predicate. Given a policy and a QA verdict, has the loop reached
@@ -21,8 +21,9 @@
21
21
  * (out of scope for #683) can reuse `runReadyGate` directly. The command shell
22
22
  * lives in `src/commands/ready.ts`.
23
23
  */
24
+ import { withEscalatedEffort, } from "./effort-escalation.js";
24
25
  import { snapshotLoopProgress, compareLoopProgress, } from "./qa-stagnation.js";
25
- import { hasExecChanges } from "./phase-executor.js";
26
+ import { classifyExecChanges } from "./phase-executor.js";
26
27
  import { readTokenUsageFiles, aggregateTokenUsage, TOKEN_USAGE_DIR, } from "./token-utils.js";
27
28
  import * as path from "path";
28
29
  /**
@@ -167,6 +168,11 @@ function buildPhaseConfig(opts, extra) {
167
168
  dryRun: false,
168
169
  mcp: opts.mcp,
169
170
  retry: true,
171
+ // #914: producer 2 (see the doc comment on RunReadyGateOptions.phasePolicies
172
+ // for why this can't drift from buildExecutionConfig's own assignment).
173
+ phasePolicies: opts.phasePolicies,
174
+ // #915: producer 2 (see RunReadyGateOptions.effortEscalation).
175
+ effortEscalation: opts.effortEscalation,
170
176
  ...extra,
171
177
  };
172
178
  }
@@ -180,7 +186,11 @@ export function formatReadyReport(result) {
180
186
  ? "✅ READY — awaiting human merge decision"
181
187
  : result.reason === "NO_IMPLEMENTATION"
182
188
  ? "⛔ NOT READY — no implementation detected"
183
- : "⚠️ NOT READY — needs human intervention";
189
+ : result.reason === "UNCOMMITTED_ONLY"
190
+ ? "⛔ NOT READY — work is uncommitted"
191
+ : result.reason === "NO_VERDICT"
192
+ ? "⛔ NOT READY — QA produced no verdict"
193
+ : "⚠️ NOT READY — needs human intervention";
184
194
  const reasonText = {
185
195
  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
196
  READY_FOR_MERGE: "QA returned READY_FOR_MERGE.",
@@ -188,7 +198,9 @@ export function formatReadyReport(result) {
188
198
  TOKEN_BUDGET: "Token budget exhausted before reaching the policy threshold. A human should review the remaining gaps.",
189
199
  LOOP_NO_DIFF: "The fix loop made no diff (stagnation guard). Manual intervention required.",
190
200
  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).",
201
+ NO_IMPLEMENTATION: "Zero-diff worktree — there is nothing to certify (#534 guard).",
202
+ 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.",
203
+ 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
204
  };
193
205
  const lines = [];
194
206
  lines.push(`## sequant ready — Issue #${result.issueNumber}`);
@@ -231,7 +243,7 @@ export function formatReadyReport(result) {
231
243
  export async function runReadyGate(opts) {
232
244
  const { issueNumber, worktreePath, policy, maxIterations, tokenBudget, nonGoals = [], } = opts;
233
245
  const readTokensUsed = opts.readTokensUsed ?? defaultReadTokensUsed;
234
- const hasChangesFn = opts.hasChangesFn ?? hasExecChanges;
246
+ const classifyChangesFn = opts.classifyChangesFn ?? classifyExecChanges;
235
247
  const snapshotFn = opts.snapshotFn ?? snapshotLoopProgress;
236
248
  // #697: run a phase while emitting live-progress events around it. `iteration`
237
249
  // is the 1-based QA-pass index so the renderer can render `loop N/M`. Emits
@@ -262,6 +274,9 @@ export async function runReadyGate(opts) {
262
274
  const autoFixed = [];
263
275
  let remaining = [];
264
276
  let tokensUsed = 0;
277
+ // #915: escalated (base, escalated) tiers, one entry per QA-pass dispatch
278
+ // that actually escalated. Populated at the two dispatch sites below.
279
+ const effortEscalations = [];
265
280
  const finish = (reason) => {
266
281
  const ready = reason === "AC_MET" || reason === "READY_FOR_MERGE";
267
282
  const issueStatus = ready
@@ -279,6 +294,7 @@ export async function runReadyGate(opts) {
279
294
  remaining,
280
295
  tokensUsed,
281
296
  report: "",
297
+ effortEscalations,
282
298
  };
283
299
  result.report = formatReadyReport(result);
284
300
  return result;
@@ -293,18 +309,33 @@ export async function runReadyGate(opts) {
293
309
  return finish("TOKEN_BUDGET");
294
310
  }
295
311
  iterations++;
296
- const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
312
+ // #915: iterations > 1 means this QA pass is a retry of a prior
313
+ // unsatisfied verdict — the ready-gate's retry signal.
314
+ const qaEscalation = withEscalatedEffort(buildPhaseConfig(opts, { fullQa: true }), "qa", iterations > 1);
315
+ if (qaEscalation.record)
316
+ effortEscalations.push(qaEscalation.record);
317
+ const qaResult = await runPhaseTracked("qa", qaEscalation.config, iterations);
297
318
  tokensUsed = readTokensUsed(worktreePath);
298
319
  const verdict = qaResult.verdict ?? null;
299
- // #534 guard: a null/unparseable verdict is never "ready".
320
+ // #534 guard: a null verdict is never "ready". #853: report it as
321
+ // NO_VERDICT, not NO_IMPLEMENTATION — a deferred/unparseable QA turn says
322
+ // nothing about whether an implementation exists, and the misleading label
323
+ // sent debuggers to the wrong place (same class as the phase-executor
324
+ // "unparseable verdict" split).
300
325
  if (!verdict) {
301
- return finish("NO_IMPLEMENTATION");
326
+ return finish("NO_VERDICT");
302
327
  }
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)) {
328
+ // #534/#879 guard: a worktree with no commits is never "ready". Split by
329
+ // cause so the report is not misleading: `none` is the empty-branch class
330
+ // (#529/#570), `uncommitted` is a dirty tree whose work cannot become a PR
331
+ // until committed (#879). `commits`/`unknown` continue (unknown fails open).
332
+ const changes = classifyChangesFn(worktreePath);
333
+ if (changes.kind === "none") {
306
334
  return finish("NO_IMPLEMENTATION");
307
335
  }
336
+ if (changes.kind === "uncommitted") {
337
+ return finish("UNCOMMITTED_ONLY");
338
+ }
308
339
  finalVerdict = verdict;
309
340
  const gaps = qaResult.summary?.gaps ?? [];
310
341
  remaining = classifyGaps(gaps, nonGoals);
@@ -327,11 +358,17 @@ export async function runReadyGate(opts) {
327
358
  .filter((g) => !g.nonGoal)
328
359
  .map((g) => g.description);
329
360
  const before = snapshotFn(worktreePath);
330
- const loopResult = await runPhaseTracked("loop", buildPhaseConfig(opts, {
361
+ // #915: iterations > 1 means this fix pass follows a QA pass that was
362
+ // itself a retry — the same ready-gate retry signal as the qa dispatch
363
+ // above.
364
+ const loopEscalation = withEscalatedEffort(buildPhaseConfig(opts, {
331
365
  lastVerdict: verdict,
332
366
  failedAcs: fixableGaps.join("; ") || undefined,
333
367
  promptContext: buildLoopContext(policy, verdict, fixableGaps),
334
- }), iterations);
368
+ }), "loop", iterations > 1);
369
+ if (loopEscalation.record)
370
+ effortEscalations.push(loopEscalation.record);
371
+ const loopResult = await runPhaseTracked("loop", loopEscalation.config, iterations);
335
372
  tokensUsed = readTokensUsed(worktreePath);
336
373
  if (!loopResult.success) {
337
374
  return finish("LOOP_FAILED");