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
@@ -4,7 +4,7 @@
4
4
  * The renderer is event-driven: a `ProgressEvent` flows in and the renderer
5
5
  * decides whether to update the live zone, append an event line, or both.
6
6
  */
7
- export type ProgressEventKind = "start" | "complete" | "failed";
7
+ export type ProgressEventKind = "start" | "complete" | "failed" | "waiting";
8
8
  /** Raw event from batch-executor `emitProgressLine` / `onProgress` callbacks. */
9
9
  export interface ProgressEvent {
10
10
  issue: number;
@@ -19,15 +19,34 @@ export interface ProgressEvent {
19
19
  * cell as `loop N/M` (#624 Item 3).
20
20
  */
21
21
  iteration?: number;
22
+ /**
23
+ * `"waiting"` (#804): display message for an in-progress auto-wait.
24
+ */
25
+ text?: string;
26
+ /**
27
+ * `"waiting"` (#804): epoch ms at which the auto-wait ends. Absent on the
28
+ * terminal notice, which returns the phase to `running`.
29
+ */
30
+ wakeAtMs?: number;
22
31
  }
23
32
  /** Per-phase status tracked inside the renderer state machine. */
24
33
  export interface PhaseState {
25
34
  name: string;
26
- status: "pending" | "running" | "done" | "failed";
35
+ /**
36
+ * `"waiting"` (#804) is a *paused* running phase, not a terminal state: the
37
+ * phase spawn has failed on an exhausted rate-limit window and the executor
38
+ * is sleeping until it reopens. It returns to `"running"` on wake.
39
+ */
40
+ status: "pending" | "running" | "done" | "failed" | "waiting";
27
41
  startedAt?: number;
28
42
  durationMs?: number;
29
43
  /** Loop iteration label (e.g. "loop 2/3"). */
30
44
  loopIteration?: number;
45
+ /**
46
+ * #804: epoch ms at which an in-progress auto-wait ends. Set only while
47
+ * `status === "waiting"`; cleared on resume.
48
+ */
49
+ wakeAtMs?: number;
31
50
  /**
32
51
  * #624 Item 4: normalized signature of the most recent failure for THIS
33
52
  * phase (ANSI-stripped, lowercased, first 80 chars, trimmed). Per-phase so
@@ -98,6 +117,8 @@ export interface IssueSummary {
98
117
  loopTriggered?: boolean;
99
118
  prNumber?: number;
100
119
  prUrl?: string;
120
+ /** Set when PR creation was skipped for lack of commits ahead of base (#920). */
121
+ prSkippedReason?: string;
101
122
  failureReason?: string;
102
123
  qaVerdict?: string;
103
124
  unmetCount?: number;
@@ -191,11 +212,6 @@ export interface RenderOptions {
191
212
  * tracks the configured maximum instead of being hardcoded. Defaults to 3.
192
213
  */
193
214
  maxLoopIterations?: number;
194
- /**
195
- * When true, `renderSummary` is rendered even if no issues were registered.
196
- * Default: false (matches existing displaySummary behaviour).
197
- */
198
- alwaysRenderSummary?: boolean;
199
215
  /**
200
216
  * #647: inject a `log-update` instance (typically built via
201
217
  * `createLogUpdate(stream)` against a custom stream). Used by the
@@ -88,6 +88,8 @@ export declare class NonTTYRenderer extends BaseRenderer {
88
88
  private readonly columnsOverride?;
89
89
  private readonly maxLoopIterations;
90
90
  private lastEventAt;
91
+ /** #860 AC-6: `issue:phase` keys whose auto-wait start line was emitted. */
92
+ private readonly announcedWaits;
91
93
  constructor(options: RenderOptions);
92
94
  private getColumns;
93
95
  private startHeartbeat;
@@ -95,6 +97,14 @@ export declare class NonTTYRenderer extends BaseRenderer {
95
97
  tickHeartbeatNow(): void;
96
98
  private tickHeartbeat;
97
99
  protected afterEvent(event: ProgressEvent, state: IssueState): void;
100
+ /**
101
+ * Announce auto-wait transitions (#860 AC-6). Only the first notice of a
102
+ * wait and the terminal notice that ends it produce lines — the ~15s ticks
103
+ * in between are absorbed here (the periodic signal is `tickHeartbeat`).
104
+ * Before this, waiting events fell through `emitEventLine`'s failure branch
105
+ * and printed a spurious `✘` line per tick.
106
+ */
107
+ private emitWaitTransitionLine;
98
108
  private emitEventLine;
99
109
  /** Append a single `\n`-terminated line with `[HH:MM:SS]` prefix. */
100
110
  private emitLine;
@@ -19,6 +19,7 @@ import chalk from "chalk";
19
19
  import logUpdate from "log-update";
20
20
  import stringWidth from "string-width";
21
21
  import { formatElapsedTime, formatTimestamp } from "./format.js";
22
+ import { formatResetTime } from "../errors.js";
22
23
  import { pipelineHasFailed } from "../workflow/status-derivation.js";
23
24
  const DEFAULT_LIVE_TICK_MS = 1000;
24
25
  const DEFAULT_NON_TTY_HEARTBEAT_MS = 60_000;
@@ -229,6 +230,23 @@ class BaseRenderer {
229
230
  state.subStatus = undefined;
230
231
  return;
231
232
  }
233
+ // #804 AC-7: an auto-wait pauses a phase rather than ending it. Presence of
234
+ // `wakeAtMs` distinguishes an in-progress wait from the terminal notice
235
+ // that resumes it, so the live zone never strands a "waiting" cell.
236
+ if (event.event === "waiting") {
237
+ if (event.wakeAtMs !== undefined) {
238
+ phase.status = "waiting";
239
+ phase.wakeAtMs = event.wakeAtMs;
240
+ state.subStatus = event.text;
241
+ }
242
+ else {
243
+ if (phase.status === "waiting")
244
+ phase.status = "running";
245
+ phase.wakeAtMs = undefined;
246
+ state.subStatus = undefined;
247
+ }
248
+ return;
249
+ }
232
250
  if (event.event === "complete") {
233
251
  phase.status = "done";
234
252
  if (event.durationSeconds !== undefined) {
@@ -329,6 +347,8 @@ export class NonTTYRenderer extends BaseRenderer {
329
347
  columnsOverride;
330
348
  maxLoopIterations;
331
349
  lastEventAt;
350
+ /** #860 AC-6: `issue:phase` keys whose auto-wait start line was emitted. */
351
+ announcedWaits = new Set();
332
352
  constructor(options) {
333
353
  super(options);
334
354
  this.heartbeatMs =
@@ -367,15 +387,53 @@ export class NonTTYRenderer extends BaseRenderer {
367
387
  return;
368
388
  const parts = running.map((s) => {
369
389
  const elapsedSec = s.startedAt !== undefined ? (this.now() - s.startedAt) / 1000 : 0;
390
+ // #860 AC-6: a phase paused on an auto-wait names its wake time instead
391
+ // of masquerading as ordinary running time — this heartbeat is the only
392
+ // periodic signal a non-TTY (background) run gets during a multi-hour
393
+ // wait, and without the wake time it is indistinguishable from a hang.
394
+ const waiting = s.phases.find((p) => p.name === s.currentPhase && p.status === "waiting");
395
+ if (waiting?.wakeAtMs !== undefined) {
396
+ const leftSec = Math.max(0, (waiting.wakeAtMs - this.now()) / 1000);
397
+ return `#${s.issueNumber} ${s.currentPhase} ⏸ rate-limit window — resuming at ${formatResetTime(waiting.wakeAtMs)} (${formatElapsedTime(leftSec)} left)`;
398
+ }
370
399
  return `#${s.issueNumber} ${s.currentPhase} (${formatElapsedTime(elapsedSec)})`;
371
400
  });
372
401
  this.emitLine(`⏱ still running: ${parts.join(", ")}`);
373
402
  }
374
403
  afterEvent(event, state) {
375
404
  super.afterEvent(event, state);
405
+ // #860 AC-6: waiting notices are liveness, not progress. The wait emits a
406
+ // tick every ~15s, so counting them as events would hold `lastEventAt`
407
+ // fresh forever and silence the heartbeat for the entire multi-hour wait.
408
+ if (event.event === "waiting") {
409
+ this.emitWaitTransitionLine(event);
410
+ return;
411
+ }
376
412
  this.lastEventAt = this.now();
377
413
  this.emitEventLine(event, state);
378
414
  }
415
+ /**
416
+ * Announce auto-wait transitions (#860 AC-6). Only the first notice of a
417
+ * wait and the terminal notice that ends it produce lines — the ~15s ticks
418
+ * in between are absorbed here (the periodic signal is `tickHeartbeat`).
419
+ * Before this, waiting events fell through `emitEventLine`'s failure branch
420
+ * and printed a spurious `✘` line per tick.
421
+ */
422
+ emitWaitTransitionLine(event) {
423
+ const key = `${event.issue}:${event.phase}`;
424
+ const c = colorize(this.noColor);
425
+ if (event.wakeAtMs !== undefined) {
426
+ if (this.announcedWaits.has(key))
427
+ return;
428
+ this.announcedWaits.add(key);
429
+ this.emitLine(`${c.yellow("⏸")} #${event.issue} ${event.phase} ${event.text ?? `rate-limit window — resuming at ${formatResetTime(event.wakeAtMs)}`}`);
430
+ }
431
+ else {
432
+ if (!this.announcedWaits.delete(key))
433
+ return;
434
+ this.emitLine(`${c.cyan("▸")} #${event.issue} ${event.phase} ${event.text ?? "auto-wait complete — resuming"}`);
435
+ }
436
+ }
379
437
  emitEventLine(event, state) {
380
438
  const c = colorize(this.noColor);
381
439
  const phase = state.phases.find((p) => p.name === event.phase);
@@ -1135,6 +1193,10 @@ export class TTYRenderer extends BaseRenderer {
1135
1193
  return c.green(`${p.name} ✔${p.durationMs ? ` ${formatElapsedTime(p.durationMs / 1000)}` : ""}`);
1136
1194
  if (p.status === "failed")
1137
1195
  return c.red(`${p.name} ✘`);
1196
+ // #804: a paused phase reads as `qa waiting 14:33`, so the wake time
1197
+ // is visible in the live zone itself, not only the sub-status line.
1198
+ if (p.status === "waiting")
1199
+ return c.yellow(`${p.name} waiting${p.wakeAtMs ? ` ${formatResetTime(p.wakeAtMs)}` : ""}`);
1138
1200
  if (p.status === "running")
1139
1201
  return c.cyan(`${p.name} running`);
1140
1202
  // #672 AC-3: pending cells render as `name –` (en dash) so the live
@@ -1270,7 +1332,13 @@ function renderSummaryDetail(r, ctx) {
1270
1332
  .map((p) => (p.success ? c.green(p.name) : c.red(p.name)))
1271
1333
  .join(" → ");
1272
1334
  const pr = r.prNumber ? ` · PR #${r.prNumber}` : "";
1273
- return { summary: `${phaseSeq}${pr}`, extras: [] };
1335
+ // #920: a phase-restricted run (no PR, no failure) still needs the reason
1336
+ // stated — otherwise a passing spec-only run looks indistinguishable from
1337
+ // one where PR creation was simply never attempted for no stated reason.
1338
+ const extras = !r.prNumber && r.prSkippedReason
1339
+ ? [c.dim(`PR skipped — ${r.prSkippedReason}`)]
1340
+ : [];
1341
+ return { summary: `${phaseSeq}${pr}`, extras };
1274
1342
  }
1275
1343
  // Failed → multi-line detail.
1276
1344
  const reason = r.failureReason ?? "failure";
@@ -145,15 +145,65 @@ export interface RateLimitInfoLike {
145
145
  hasChargeableSavedPaymentMethod?: boolean;
146
146
  }
147
147
  /**
148
- * True when the rate-limit info represents a billing/credits failure (which a
149
- * retry cannot fix), rather than a transient throttle.
148
+ * True when the rate-limit info describes an exhausted *window* that will
149
+ * reopen at a known future time: a recognized window `rateLimitType` plus a
150
+ * `resetsAt` still in the future (#860 AC-1).
151
+ *
152
+ * This is the discriminator between "subscription window closed until 07:00"
153
+ * and "account needs credits". The fields that would answer that directly
154
+ * (`canUserPurchaseCredits`, `hasChargeableSavedPaymentMethod`) are absent
155
+ * from every real captured payload, so the window shape is the proxy — and
156
+ * any unrecognized shape returns false (fail closed, #860 AC-3).
157
+ *
158
+ * Explicitly informational statuses (`allowed` / `allowed_warning`) are never
159
+ * waitable. The driver retains marker-carrying warnings as failure-grade
160
+ * (pre-#732 semantics, unchanged), so a stale "you are nearing your limit"
161
+ * warning can be misattributed to an unrelated phase failure — classifying it
162
+ * waitable would upgrade that misattribution from a cheap immediate halt to a
163
+ * multi-hour sleep. The 26 real captures carry no `status` field at all, so
164
+ * absent status stays waitable; only a status that affirmatively says
165
+ * "not a rejection" is excluded.
166
+ */
167
+ export declare function isWaitableWindow(info: RateLimitInfoLike, now?: number): boolean;
168
+ /**
169
+ * Vocabulary-drift telemetry for the #860 fail-closed path: returns a hint
170
+ * string when a payload was terminal ONLY because its `rateLimitType` is not
171
+ * in the recognized window allowlist — i.e. it carries `out_of_credits` plus a
172
+ * live future `resetsAt` and a *present but unrecognized* window type, and no
173
+ * explicit `credits_required`. If Anthropic renames or adds a window type,
174
+ * the halt message names the rejected type instead of silently reading as an
175
+ * ordinary wallet failure. A missing `rateLimitType` is NOT drift evidence
176
+ * (API-account payloads may legitimately omit it), so no hint fires there.
177
+ */
178
+ export declare function unrecognizedWindowHint(info: RateLimitInfoLike, now?: number): string | null;
179
+ /**
180
+ * True when the rate-limit info represents a billing/credits failure (which
181
+ * waiting cannot fix), rather than a transient throttle or an exhausted
182
+ * window.
183
+ *
184
+ * Narrowed by #860: a subscription plan hitting its five-hour cap with
185
+ * overage disabled emits `overageDisabledReason: "out_of_credits"` *plus* a
186
+ * window type and a live `resetsAt` — a pause, not a wallet failure. That
187
+ * shape is excluded here so it classifies as a retryable {@link RateLimitError}
188
+ * and `--auto-wait` (#804) can act on it. An explicit
189
+ * `errorCode: "credits_required"` stays terminal even alongside window
190
+ * evidence — it is the SDK's direct "purchase needed" signal (#860 AC-2).
191
+ * Anything short of the full recognized window shape remains terminal
192
+ * (fail closed, #860 AC-3).
150
193
  */
151
- export declare function isBillingFailure(info: RateLimitInfoLike): boolean;
194
+ export declare function isBillingFailure(info: RateLimitInfoLike, now?: number): boolean;
152
195
  /**
153
196
  * True when the rate-limit info represents an actual failure (rejection or
154
197
  * billing), as opposed to an informational `allowed` / `allowed_warning`
155
198
  * event. The driver uses this to avoid mis-attributing a stale warning event
156
199
  * to an unrelated phase failure.
200
+ *
201
+ * Built on the raw billing *markers*, not the #860-narrowed classification:
202
+ * the captured five-hour payloads cannot prove they carried
203
+ * `status: "rejected"`, so narrowing here could silently drop the very events
204
+ * #860 exists to keep (they'd fall back to a metadata-less assistant error and
205
+ * auto-wait would stay inert). Retention semantics are unchanged; only the
206
+ * billing-vs-waitable *classification* narrowed.
157
207
  */
158
208
  export declare function isRateLimitFailureInfo(info: RateLimitInfoLike): boolean;
159
209
  /**
@@ -162,6 +212,20 @@ export declare function isRateLimitFailureInfo(info: RateLimitInfoLike): boolean
162
212
  * displayed: values below ~1e12 are seconds, otherwise milliseconds.
163
213
  */
164
214
  export declare function resetsAtToMs(resetsAt: number): number;
215
+ /**
216
+ * Format a Unix timestamp (seconds or ms) as a local time string.
217
+ *
218
+ * Bare `HH:MM` when the reset falls on the current local calendar day;
219
+ * date-qualified `MM-DD HH:MM` otherwise. Also used for #804's auto-wait wake
220
+ * time (a wake is `resetsAt + buffer`, already in ms, which `resetsAtToMs`
221
+ * passes through unchanged) so both render in one convention.
222
+ *
223
+ * Multi-day windows
224
+ * (`rateLimitType: seven_day*`) can reset days out — a bare `HH:MM` there reads
225
+ * as "later today" and misleads the user (#732 QA follow-up), so the date is
226
+ * included whenever the reset is not today.
227
+ */
228
+ export declare function formatResetTime(resetsAt: number): string;
165
229
  /**
166
230
  * Build a user-facing message from rate-limit info, naming the real cause:
167
231
  * - billing/credits → "Out of credits" (enriched with purchasable vs hard
@@ -169,14 +233,22 @@ export declare function resetsAtToMs(resetsAt: number): number;
169
233
  * - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
170
234
  * "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
171
235
  * `resetsAt` is absent)
236
+ *
237
+ * `now` feeds the #860 waitable-window classification so message and error
238
+ * type are derived against the same instant (and tests can pin the clock).
239
+ * A waitable window renders through the rate-limited branch — its reset time
240
+ * is the actionable fact; "Out of credits" would misname a pause as a wallet
241
+ * failure.
172
242
  */
173
- export declare function formatRateLimitMessage(info: RateLimitInfoLike): string;
243
+ export declare function formatRateLimitMessage(info: RateLimitInfoLike, now?: number): string;
174
244
  /**
175
245
  * Construct the appropriate typed error from structured rate-limit info.
176
246
  * Billing/credits failures become a non-retryable {@link BillingError};
177
- * transient throttles become a retryable {@link RateLimitError}.
247
+ * transient throttles AND exhausted-but-reopening windows (#860) become a
248
+ * retryable {@link RateLimitError}. `now` pins the waitable-window check to
249
+ * one instant across message and classification.
178
250
  */
179
- export declare function createRateLimitError(info: RateLimitInfoLike): RateLimitError | BillingError;
251
+ export declare function createRateLimitError(info: RateLimitInfoLike, now?: number): RateLimitError | BillingError;
180
252
  /**
181
253
  * Map of error type names to their constructors.
182
254
  * Used for deserialization from logs.
@@ -107,21 +107,122 @@ export class BillingError extends SequantError {
107
107
  }
108
108
  }
109
109
  /**
110
- * True when the rate-limit info represents a billing/credits failure (which a
111
- * retry cannot fix), rather than a transient throttle.
110
+ * Recognized rate-limit *window* vocabulary limit types whose exhaustion is
111
+ * a pause that reopens at `resetsAt`, not a wallet state. `five_hour` is the
112
+ * subscription session window; `seven_day` is prefix-matched because the SDK
113
+ * emits model-qualified variants (`seven_day*`, see {@link formatResetTime}).
114
+ *
115
+ * Deliberately an allowlist (#860 AC-3): this encodes an inference about
116
+ * Anthropic's payload vocabulary, so an unlisted type (e.g. `overage`) must
117
+ * fail closed to the terminal billing path rather than into a five-hour sleep.
118
+ */
119
+ const WAITABLE_WINDOW_TYPE_RE = /^(five_hour|seven_day)/;
120
+ /**
121
+ * True when the rate-limit info describes an exhausted *window* that will
122
+ * reopen at a known future time: a recognized window `rateLimitType` plus a
123
+ * `resetsAt` still in the future (#860 AC-1).
124
+ *
125
+ * This is the discriminator between "subscription window closed until 07:00"
126
+ * and "account needs credits". The fields that would answer that directly
127
+ * (`canUserPurchaseCredits`, `hasChargeableSavedPaymentMethod`) are absent
128
+ * from every real captured payload, so the window shape is the proxy — and
129
+ * any unrecognized shape returns false (fail closed, #860 AC-3).
130
+ *
131
+ * Explicitly informational statuses (`allowed` / `allowed_warning`) are never
132
+ * waitable. The driver retains marker-carrying warnings as failure-grade
133
+ * (pre-#732 semantics, unchanged), so a stale "you are nearing your limit"
134
+ * warning can be misattributed to an unrelated phase failure — classifying it
135
+ * waitable would upgrade that misattribution from a cheap immediate halt to a
136
+ * multi-hour sleep. The 26 real captures carry no `status` field at all, so
137
+ * absent status stays waitable; only a status that affirmatively says
138
+ * "not a rejection" is excluded.
139
+ */
140
+ export function isWaitableWindow(info, now = Date.now()) {
141
+ if (info.status === "allowed" || info.status === "allowed_warning") {
142
+ return false;
143
+ }
144
+ if (typeof info.rateLimitType !== "string")
145
+ return false;
146
+ if (!WAITABLE_WINDOW_TYPE_RE.test(info.rateLimitType))
147
+ return false;
148
+ if (typeof info.resetsAt !== "number")
149
+ return false;
150
+ return resetsAtToMs(info.resetsAt) > now;
151
+ }
152
+ /**
153
+ * Vocabulary-drift telemetry for the #860 fail-closed path: returns a hint
154
+ * string when a payload was terminal ONLY because its `rateLimitType` is not
155
+ * in the recognized window allowlist — i.e. it carries `out_of_credits` plus a
156
+ * live future `resetsAt` and a *present but unrecognized* window type, and no
157
+ * explicit `credits_required`. If Anthropic renames or adds a window type,
158
+ * the halt message names the rejected type instead of silently reading as an
159
+ * ordinary wallet failure. A missing `rateLimitType` is NOT drift evidence
160
+ * (API-account payloads may legitimately omit it), so no hint fires there.
161
+ */
162
+ export function unrecognizedWindowHint(info, now = Date.now()) {
163
+ if (info.errorCode === "credits_required")
164
+ return null;
165
+ if (info.overageDisabledReason !== "out_of_credits")
166
+ return null;
167
+ if (info.status === "allowed" || info.status === "allowed_warning") {
168
+ return null;
169
+ }
170
+ if (typeof info.rateLimitType !== "string")
171
+ return null;
172
+ if (WAITABLE_WINDOW_TYPE_RE.test(info.rateLimitType))
173
+ return null;
174
+ if (typeof info.resetsAt !== "number")
175
+ return null;
176
+ if (resetsAtToMs(info.resetsAt) <= now)
177
+ return null;
178
+ return `unrecognized window type "${info.rateLimitType}" with a future reset — treated as terminal (auto-wait recognizes five_hour/seven_day)`;
179
+ }
180
+ /**
181
+ * True when the info carries an explicit billing/credits marker, regardless
182
+ * of whether a live window would make it waitable. This is the pre-#860
183
+ * `isBillingFailure` predicate, kept for failure-*detection* sites
184
+ * ({@link isRateLimitFailureInfo}) whose retention semantics must not narrow.
112
185
  */
113
- export function isBillingFailure(info) {
186
+ function hasBillingMarkers(info) {
114
187
  return (info.errorCode === "credits_required" ||
115
188
  info.overageDisabledReason === "out_of_credits");
116
189
  }
190
+ /**
191
+ * True when the rate-limit info represents a billing/credits failure (which
192
+ * waiting cannot fix), rather than a transient throttle or an exhausted
193
+ * window.
194
+ *
195
+ * Narrowed by #860: a subscription plan hitting its five-hour cap with
196
+ * overage disabled emits `overageDisabledReason: "out_of_credits"` *plus* a
197
+ * window type and a live `resetsAt` — a pause, not a wallet failure. That
198
+ * shape is excluded here so it classifies as a retryable {@link RateLimitError}
199
+ * and `--auto-wait` (#804) can act on it. An explicit
200
+ * `errorCode: "credits_required"` stays terminal even alongside window
201
+ * evidence — it is the SDK's direct "purchase needed" signal (#860 AC-2).
202
+ * Anything short of the full recognized window shape remains terminal
203
+ * (fail closed, #860 AC-3).
204
+ */
205
+ export function isBillingFailure(info, now = Date.now()) {
206
+ if (info.errorCode === "credits_required")
207
+ return true;
208
+ return (info.overageDisabledReason === "out_of_credits" &&
209
+ !isWaitableWindow(info, now));
210
+ }
117
211
  /**
118
212
  * True when the rate-limit info represents an actual failure (rejection or
119
213
  * billing), as opposed to an informational `allowed` / `allowed_warning`
120
214
  * event. The driver uses this to avoid mis-attributing a stale warning event
121
215
  * to an unrelated phase failure.
216
+ *
217
+ * Built on the raw billing *markers*, not the #860-narrowed classification:
218
+ * the captured five-hour payloads cannot prove they carried
219
+ * `status: "rejected"`, so narrowing here could silently drop the very events
220
+ * #860 exists to keep (they'd fall back to a metadata-less assistant error and
221
+ * auto-wait would stay inert). Retention semantics are unchanged; only the
222
+ * billing-vs-waitable *classification* narrowed.
122
223
  */
123
224
  export function isRateLimitFailureInfo(info) {
124
- return info.status === "rejected" || isBillingFailure(info);
225
+ return info.status === "rejected" || hasBillingMarkers(info);
125
226
  }
126
227
  /**
127
228
  * Normalize a `resetsAt` timestamp to milliseconds. The SDK does not pin the
@@ -135,12 +236,16 @@ export function resetsAtToMs(resetsAt) {
135
236
  * Format a Unix timestamp (seconds or ms) as a local time string.
136
237
  *
137
238
  * Bare `HH:MM` when the reset falls on the current local calendar day;
138
- * date-qualified `MM-DD HH:MM` otherwise. Multi-day windows
239
+ * date-qualified `MM-DD HH:MM` otherwise. Also used for #804's auto-wait wake
240
+ * time (a wake is `resetsAt + buffer`, already in ms, which `resetsAtToMs`
241
+ * passes through unchanged) so both render in one convention.
242
+ *
243
+ * Multi-day windows
139
244
  * (`rateLimitType: seven_day*`) can reset days out — a bare `HH:MM` there reads
140
245
  * as "later today" and misleads the user (#732 QA follow-up), so the date is
141
246
  * included whenever the reset is not today.
142
247
  */
143
- function formatResetTime(resetsAt) {
248
+ export function formatResetTime(resetsAt) {
144
249
  const ms = resetsAtToMs(resetsAt);
145
250
  const d = new Date(ms);
146
251
  const hh = String(d.getHours()).padStart(2, "0");
@@ -163,16 +268,27 @@ function formatResetTime(resetsAt) {
163
268
  * - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
164
269
  * "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
165
270
  * `resetsAt` is absent)
271
+ *
272
+ * `now` feeds the #860 waitable-window classification so message and error
273
+ * type are derived against the same instant (and tests can pin the clock).
274
+ * A waitable window renders through the rate-limited branch — its reset time
275
+ * is the actionable fact; "Out of credits" would misname a pause as a wallet
276
+ * failure.
166
277
  */
167
- export function formatRateLimitMessage(info) {
168
- if (isBillingFailure(info)) {
278
+ export function formatRateLimitMessage(info, now = Date.now()) {
279
+ if (isBillingFailure(info, now)) {
280
+ // #860 drift telemetry: when the ONLY reason this is terminal is an
281
+ // unrecognized window type, say so — the message is the one channel that
282
+ // reaches run output, logs, and `PhaseResult.error` on every display path.
283
+ const hint = unrecognizedWindowHint(info, now);
284
+ const suffix = hint ? ` (${hint})` : "";
169
285
  if (info.canUserPurchaseCredits === true) {
170
- return "Out of credits — purchasable";
286
+ return `Out of credits — purchasable${suffix}`;
171
287
  }
172
288
  if (info.canUserPurchaseCredits === false) {
173
- return "Out of credits — hard limit";
289
+ return `Out of credits — hard limit${suffix}`;
174
290
  }
175
- return "Out of credits";
291
+ return `Out of credits${suffix}`;
176
292
  }
177
293
  if (info.resetsAt !== undefined) {
178
294
  return `Rate limited — resets at ${formatResetTime(info.resetsAt)}`;
@@ -182,10 +298,12 @@ export function formatRateLimitMessage(info) {
182
298
  /**
183
299
  * Construct the appropriate typed error from structured rate-limit info.
184
300
  * Billing/credits failures become a non-retryable {@link BillingError};
185
- * transient throttles become a retryable {@link RateLimitError}.
301
+ * transient throttles AND exhausted-but-reopening windows (#860) become a
302
+ * retryable {@link RateLimitError}. `now` pins the waitable-window check to
303
+ * one instant across message and classification.
186
304
  */
187
- export function createRateLimitError(info) {
188
- const message = formatRateLimitMessage(info);
305
+ export function createRateLimitError(info, now = Date.now()) {
306
+ const message = formatRateLimitMessage(info, now);
189
307
  const metadata = {
190
308
  resetsAt: info.resetsAt,
191
309
  rateLimitType: info.rateLimitType,
@@ -194,7 +312,7 @@ export function createRateLimitError(info) {
194
312
  canUserPurchaseCredits: info.canUserPurchaseCredits,
195
313
  hasChargeableSavedPaymentMethod: info.hasChargeableSavedPaymentMethod,
196
314
  };
197
- return isBillingFailure(info)
315
+ return isBillingFailure(info, now)
198
316
  ? new BillingError(message, metadata)
199
317
  : new RateLimitError(message, metadata);
200
318
  }