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
@@ -18,10 +18,17 @@ export function formatOutputs(result) {
18
18
  }
19
19
  /**
20
20
  * Format multiple issue results into combined outputs.
21
+ *
22
+ * @param wallClockDurationSeconds - The run's wall-clock duration, computed by
23
+ * whoever brackets the run (the orchestrator surfaces it as
24
+ * `RunResult.wallClockDurationSeconds`). It is a required argument rather than
25
+ * something derived here because the per-issue durations this function receives
26
+ * cannot express it: under `--parallel` they overlap, so summing them
27
+ * double-counts and over-reports by roughly the concurrency factor (#867).
21
28
  */
22
- export function formatMultiOutputs(results) {
29
+ export function formatMultiOutputs(results, wallClockDurationSeconds) {
23
30
  const allSuccess = results.every((r) => r.success);
24
- const totalDuration = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
31
+ const totalDuration = wallClockDurationSeconds;
25
32
  const prUrls = results
26
33
  .map((r) => r.prUrl)
27
34
  .filter(Boolean)
@@ -54,8 +61,14 @@ export function outputCommands(outputs) {
54
61
  }
55
62
  /**
56
63
  * Generate a GitHub Actions step summary (Markdown).
64
+ *
65
+ * @param wallClockDurationSeconds - The run's wall-clock duration. Required for
66
+ * the same reason as in {@link formatMultiOutputs}: the per-issue durations
67
+ * below overlap under `--parallel`, so the "Total duration" line cannot be
68
+ * derived by summing them (#867). Per-*phase* durations in the tables are
69
+ * unaffected — phases within an issue run sequentially.
57
70
  */
58
- export function formatSummary(results) {
71
+ export function formatSummary(results, wallClockDurationSeconds) {
59
72
  const lines = [];
60
73
  lines.push("## Sequant Workflow Results\n");
61
74
  for (const result of results) {
@@ -78,7 +91,6 @@ export function formatSummary(results) {
78
91
  }
79
92
  lines.push("");
80
93
  }
81
- const total = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
82
- lines.push(`**Total duration:** ${total}s`);
94
+ lines.push(`**Total duration:** ${wallClockDurationSeconds}s`);
83
95
  return lines.join("\n");
84
96
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Commander argument coercions for numeric CLI flags.
3
+ *
4
+ * Lives here rather than inline in `bin/cli.ts` so it can be unit-tested
5
+ * directly — `bin/cli.ts` calls `program.parse()` at import time and cannot be
6
+ * imported from a test.
7
+ *
8
+ * @module
9
+ */
10
+ /** Error-text shaping for a numeric flag. Affects messages only, not parsing. */
11
+ export interface WholeNumberOptions {
12
+ /** Smallest accepted value. Use 0 where 0 is a meaningful "off" setting. */
13
+ min: number;
14
+ /** Plural unit, e.g. `"seconds"`. Omit for a flag that reads better bare. */
15
+ unit?: string;
16
+ /** Singular unit, used when `min` is 1 ("at least 1 second"). */
17
+ unitSingular?: string;
18
+ }
19
+ /**
20
+ * Build a commander coercion for a whole-number flag.
21
+ *
22
+ * A bare `parseInt` is unsafe for these: `parseInt("abc", 10)` is `NaN`, which
23
+ * is not nullish and so survives a `?? default`, and `parseInt("30m", 10)`
24
+ * silently yields `30` — the user asked for 30 minutes and got 30 seconds.
25
+ * Requiring the whole string to be digits rejects both instead of accepting a
26
+ * value that is unusable or quietly wrong (#818, #833).
27
+ */
28
+ export declare function parseWholeNumber(flag: string, opts: WholeNumberOptions): (value: string) => number;
29
+ /**
30
+ * A positive-integer *seconds* flag — the #831 shape, preserved verbatim so the
31
+ * `merge --watch` messages this originally shipped with do not change.
32
+ */
33
+ export declare function parsePositiveSeconds(flag: string): (value: string) => number;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Commander argument coercions for numeric CLI flags.
3
+ *
4
+ * Lives here rather than inline in `bin/cli.ts` so it can be unit-tested
5
+ * directly — `bin/cli.ts` calls `program.parse()` at import time and cannot be
6
+ * imported from a test.
7
+ *
8
+ * @module
9
+ */
10
+ import { InvalidArgumentError } from "commander";
11
+ /**
12
+ * Build a commander coercion for a whole-number flag.
13
+ *
14
+ * A bare `parseInt` is unsafe for these: `parseInt("abc", 10)` is `NaN`, which
15
+ * is not nullish and so survives a `?? default`, and `parseInt("30m", 10)`
16
+ * silently yields `30` — the user asked for 30 minutes and got 30 seconds.
17
+ * Requiring the whole string to be digits rejects both instead of accepting a
18
+ * value that is unusable or quietly wrong (#818, #833).
19
+ */
20
+ export function parseWholeNumber(flag, opts) {
21
+ const { min, unit, unitSingular } = opts;
22
+ const ofUnit = unit ? ` of ${unit}` : "";
23
+ const minUnit = min === 1 ? (unitSingular ?? unit) : unit;
24
+ const minSuffix = minUnit ? ` ${minUnit}` : "";
25
+ return (value) => {
26
+ if (!/^\d+$/.test(value.trim())) {
27
+ throw new InvalidArgumentError(`${flag} expects a whole number${ofUnit} (got '${value}').`);
28
+ }
29
+ const parsed = Number(value);
30
+ if (!Number.isSafeInteger(parsed) || parsed < min) {
31
+ throw new InvalidArgumentError(`${flag} must be at least ${min}${minSuffix} (got '${value}').`);
32
+ }
33
+ return parsed;
34
+ };
35
+ }
36
+ /**
37
+ * A positive-integer *seconds* flag — the #831 shape, preserved verbatim so the
38
+ * `merge --watch` messages this originally shipped with do not change.
39
+ */
40
+ export function parsePositiveSeconds(flag) {
41
+ return parseWholeNumber(flag, {
42
+ min: 1,
43
+ unit: "seconds",
44
+ unitSingular: "second",
45
+ });
46
+ }
@@ -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
@@ -191,11 +210,6 @@ export interface RenderOptions {
191
210
  * tracks the configured maximum instead of being hardcoded. Defaults to 3.
192
211
  */
193
212
  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
213
  /**
200
214
  * #647: inject a `log-update` instance (typically built via
201
215
  * `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,8 @@ 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";
23
+ import { pipelineHasFailed } from "../workflow/status-derivation.js";
22
24
  const DEFAULT_LIVE_TICK_MS = 1000;
23
25
  const DEFAULT_NON_TTY_HEARTBEAT_MS = 60_000;
24
26
  const NARROW_TERMINAL_THRESHOLD = 80;
@@ -228,6 +230,23 @@ class BaseRenderer {
228
230
  state.subStatus = undefined;
229
231
  return;
230
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
+ }
231
250
  if (event.event === "complete") {
232
251
  phase.status = "done";
233
252
  if (event.durationSeconds !== undefined) {
@@ -248,8 +267,13 @@ class BaseRenderer {
248
267
  else if (phase.startedAt !== undefined) {
249
268
  phase.durationMs = this.now() - phase.startedAt;
250
269
  }
251
- state.status = "failed";
252
- state.completedAt = this.now();
270
+ // #766: derive from the phase slots (loop excluded) instead of pinning
271
+ // `failed`, so a loop failure on an early quality-loop iteration doesn't
272
+ // stick after a later iteration recovers. Mirrors the orchestrator's card.
273
+ const nowFailed = pipelineHasFailed(state.phases);
274
+ state.status = nowFailed ? "failed" : "running";
275
+ if (nowFailed)
276
+ state.completedAt = this.now();
253
277
  state.currentPhase = undefined;
254
278
  if (event.error !== undefined)
255
279
  state.failureReason = event.error;
@@ -267,20 +291,22 @@ class BaseRenderer {
267
291
  }
268
292
  /** Mark an issue done after PR is recorded — derived from phase completion. */
269
293
  maybeMarkIssueDone(state) {
270
- if (state.status === "failed")
271
- return;
272
294
  const allTerminal = state.phases.every((p) => p.status === "done" || p.status === "failed");
273
295
  if (allTerminal && state.phases.length > 0) {
274
- state.status = state.phases.some((p) => p.status === "failed")
275
- ? "failed"
276
- : "done";
296
+ // #766: derive the verdict (loop excluded) so a run that failed the loop
297
+ // on an early iteration and then recovered every planned phase resolves
298
+ // to `done`. No early `failed` guard: a stale loop failure must be able
299
+ // to de-escalate once the pipeline recovers.
300
+ state.status = pipelineHasFailed(state.phases) ? "failed" : "done";
277
301
  state.completedAt = this.now();
278
302
  }
279
303
  }
280
304
  // ------------ Hooks for subclasses ------------
281
305
  afterEvent(_event, state) {
282
- if (state.status !== "failed")
283
- this.maybeMarkIssueDone(state);
306
+ // #766: always re-derive — `maybeMarkIssueDone` guards internally and must
307
+ // run even when `state.status` is currently `failed` so a recovered loop
308
+ // failure can de-escalate to `done`.
309
+ this.maybeMarkIssueDone(state);
284
310
  this.afterStateChange();
285
311
  }
286
312
  afterStateChange() {
@@ -321,6 +347,8 @@ export class NonTTYRenderer extends BaseRenderer {
321
347
  columnsOverride;
322
348
  maxLoopIterations;
323
349
  lastEventAt;
350
+ /** #860 AC-6: `issue:phase` keys whose auto-wait start line was emitted. */
351
+ announcedWaits = new Set();
324
352
  constructor(options) {
325
353
  super(options);
326
354
  this.heartbeatMs =
@@ -359,15 +387,53 @@ export class NonTTYRenderer extends BaseRenderer {
359
387
  return;
360
388
  const parts = running.map((s) => {
361
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
+ }
362
399
  return `#${s.issueNumber} ${s.currentPhase} (${formatElapsedTime(elapsedSec)})`;
363
400
  });
364
401
  this.emitLine(`⏱ still running: ${parts.join(", ")}`);
365
402
  }
366
403
  afterEvent(event, state) {
367
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
+ }
368
412
  this.lastEventAt = this.now();
369
413
  this.emitEventLine(event, state);
370
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
+ }
371
437
  emitEventLine(event, state) {
372
438
  const c = colorize(this.noColor);
373
439
  const phase = state.phases.find((p) => p.name === event.phase);
@@ -1127,6 +1193,10 @@ export class TTYRenderer extends BaseRenderer {
1127
1193
  return c.green(`${p.name} ✔${p.durationMs ? ` ${formatElapsedTime(p.durationMs / 1000)}` : ""}`);
1128
1194
  if (p.status === "failed")
1129
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)}` : ""}`);
1130
1200
  if (p.status === "running")
1131
1201
  return c.cyan(`${p.name} running`);
1132
1202
  // #672 AC-3: pending cells render as `name –` (en dash) so the live
@@ -145,17 +145,87 @@ 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.
150
177
  */
151
- export declare function isBillingFailure(info: RateLimitInfoLike): boolean;
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).
193
+ */
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;
209
+ /**
210
+ * Normalize a `resetsAt` timestamp to milliseconds. The SDK does not pin the
211
+ * unit, so use the same heuristic everywhere a `resetsAt` is compared or
212
+ * displayed: values below ~1e12 are seconds, otherwise milliseconds.
213
+ */
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;
159
229
  /**
160
230
  * Build a user-facing message from rate-limit info, naming the real cause:
161
231
  * - billing/credits → "Out of credits" (enriched with purchasable vs hard
@@ -163,14 +233,22 @@ export declare function isRateLimitFailureInfo(info: RateLimitInfoLike): boolean
163
233
  * - transient throttle → "Rate limited — resets at HH:MM" (date-qualified as
164
234
  * "MM-DD HH:MM" when the reset is not today; reset time omitted entirely when
165
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.
166
242
  */
167
- export declare function formatRateLimitMessage(info: RateLimitInfoLike): string;
243
+ export declare function formatRateLimitMessage(info: RateLimitInfoLike, now?: number): string;
168
244
  /**
169
245
  * Construct the appropriate typed error from structured rate-limit info.
170
246
  * Billing/credits failures become a non-retryable {@link BillingError};
171
- * 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.
172
250
  */
173
- export declare function createRateLimitError(info: RateLimitInfoLike): RateLimitError | BillingError;
251
+ export declare function createRateLimitError(info: RateLimitInfoLike, now?: number): RateLimitError | BillingError;
174
252
  /**
175
253
  * Map of error type names to their constructors.
176
254
  * Used for deserialization from logs.