sequant 2.7.0 → 2.9.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 (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +18 -2
  4. package/dist/bin/cli.d.ts +1 -1
  5. package/dist/bin/cli.js +12 -2
  6. package/dist/bin/preflight.d.ts +21 -0
  7. package/dist/bin/preflight.js +45 -0
  8. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  9. package/dist/marketplace/external_plugins/sequant/README.md +2 -0
  10. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
  11. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
  12. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/force-push.md +34 -0
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +117 -19
  14. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +29 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +100 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +24 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +285 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +202 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +287 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/test-quality-checklist.md +272 -0
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +40 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +95 -11
  25. package/dist/marketplace/external_plugins/sequant/skills/references/shared/framework-gotchas.md +186 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
  27. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  28. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  29. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +669 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  31. package/dist/marketplace/external_plugins/sequant/skills/test/references/browser-testing-patterns.md +423 -0
  32. package/dist/marketplace/external_plugins/sequant/skills/upstream/SKILL.md +419 -0
  33. package/dist/src/commands/logs.js +6 -1
  34. package/dist/src/commands/run-display.d.ts +20 -0
  35. package/dist/src/commands/run-display.js +80 -1
  36. package/dist/src/commands/stats.js +47 -0
  37. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  38. package/dist/src/lib/assess-collision-detect.js +68 -4
  39. package/dist/src/lib/cli-ui/run-renderer.js +17 -9
  40. package/dist/src/lib/errors.d.ts +91 -0
  41. package/dist/src/lib/errors.js +118 -0
  42. package/dist/src/lib/manifest.js +1 -17
  43. package/dist/src/lib/version-check.d.ts +19 -0
  44. package/dist/src/lib/version-check.js +45 -5
  45. package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
  46. package/dist/src/lib/workflow/batch-executor.js +142 -24
  47. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  48. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  49. package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
  50. package/dist/src/lib/workflow/chain-resume.js +166 -0
  51. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  52. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  53. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +17 -0
  54. package/dist/src/lib/workflow/drivers/claude-code.d.ts +29 -0
  55. package/dist/src/lib/workflow/drivers/claude-code.js +136 -8
  56. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  57. package/dist/src/lib/workflow/error-classifier.js +14 -1
  58. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  59. package/dist/src/lib/workflow/log-writer.js +6 -8
  60. package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
  61. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  62. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  63. package/dist/src/lib/workflow/phase-executor.d.ts +50 -0
  64. package/dist/src/lib/workflow/phase-executor.js +151 -17
  65. package/dist/src/lib/workflow/run-log-schema.d.ts +26 -0
  66. package/dist/src/lib/workflow/run-log-schema.js +52 -1
  67. package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
  68. package/dist/src/lib/workflow/run-orchestrator.js +291 -30
  69. package/dist/src/lib/workflow/state-manager.d.ts +1 -0
  70. package/dist/src/lib/workflow/state-manager.js +6 -0
  71. package/dist/src/lib/workflow/state-schema.d.ts +3 -0
  72. package/dist/src/lib/workflow/state-schema.js +7 -0
  73. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  74. package/dist/src/lib/workflow/status-derivation.js +27 -0
  75. package/dist/src/lib/workflow/types.d.ts +40 -0
  76. package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
  77. package/dist/src/lib/workflow/worktree-manager.js +103 -33
  78. package/dist/src/mcp/tools/run.d.ts +2 -0
  79. package/dist/src/mcp/tools/run.js +2 -0
  80. package/dist/src/ui/tui/theme.d.ts +18 -4
  81. package/dist/src/ui/tui/theme.js +18 -4
  82. package/package.json +5 -6
  83. package/templates/hooks/post-tool.sh +18 -3
  84. package/templates/hooks/pre-tool.sh +330 -57
  85. package/templates/scripts/cleanup-worktree.sh +103 -14
  86. package/templates/skills/_shared/references/force-push.md +34 -0
  87. package/templates/skills/assess/SKILL.md +117 -19
  88. package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
  89. package/templates/skills/exec/SKILL.md +29 -0
  90. package/templates/skills/fullsolve/SKILL.md +1 -1
  91. package/templates/skills/loop/SKILL.md +100 -2
  92. package/templates/skills/qa/SKILL.md +24 -0
  93. package/templates/skills/qa/references/anti-pattern-detection.md +285 -0
  94. package/templates/skills/qa/references/call-site-review.md +202 -0
  95. package/templates/skills/qa/references/quality-gates.md +287 -0
  96. package/templates/skills/qa/references/test-quality-checklist.md +272 -0
  97. package/templates/skills/qa/references/testing-requirements.md +40 -0
  98. package/templates/skills/qa/scripts/quality-checks.sh +95 -11
  99. package/templates/skills/references/shared/framework-gotchas.md +186 -0
  100. package/templates/skills/reflect/SKILL.md +27 -13
  101. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  102. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  103. package/templates/skills/release/SKILL.md +669 -0
  104. package/templates/skills/spec/references/verification-criteria.md +1 -1
  105. package/templates/skills/test/references/browser-testing-patterns.md +423 -0
  106. package/templates/skills/upstream/SKILL.md +419 -0
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { query } from "@anthropic-ai/claude-agent-sdk";
8
8
  import { getMcpServersConfig } from "../../system.js";
9
+ import { RateLimitError, BillingError, createRateLimitError, isRateLimitFailureInfo, } from "../../errors.js";
9
10
  import { RingBuffer } from "../ring-buffer.js";
10
11
  export class ClaudeCodeDriver {
11
12
  name = "claude-code";
@@ -47,6 +48,16 @@ export class ClaudeCodeDriver {
47
48
  let resultMessage;
48
49
  let capturedOutput = "";
49
50
  let capturedStderr = "";
51
+ // Structured rate-limit / billing signals captured from the SDK stream
52
+ // (#732). The SDK emits these but sequant previously dropped them on the
53
+ // floor, falling back to regex-on-stderr classification. We keep only the
54
+ // latest *failure-grade* rate-limit info (rejection or billing) so an
55
+ // informational `allowed_warning` event isn't mis-attributed to an
56
+ // unrelated phase failure.
57
+ let rateLimitInfo;
58
+ let assistantError;
59
+ // Last api_retry signal, captured opportunistically for diagnostics.
60
+ let apiRetryError;
50
61
  const stderrBuffer = new RingBuffer(50);
51
62
  const stdoutBuffer = new RingBuffer(50);
52
63
  // Resolve resume token with cwd-safety check.
@@ -99,7 +110,26 @@ export class ClaudeCodeDriver {
99
110
  if (message.type === "system" && message.subtype === "init") {
100
111
  resultSessionId = message.session_id;
101
112
  }
113
+ // Capture structured rate-limit info (#732). Only retain
114
+ // failure-grade events (rejection / billing) so a benign warning
115
+ // doesn't poison the failure path.
116
+ if (message.type === "rate_limit_event" &&
117
+ isRateLimitFailureInfo(message.rate_limit_info)) {
118
+ rateLimitInfo = message.rate_limit_info;
119
+ }
120
+ // Capture api_retry diagnostics (#732, optional). These are transient
121
+ // retries the SDK performs internally; recorded for the structured
122
+ // error fallback when no rate_limit_event/assistant error is present.
123
+ if (message.type === "system" && message.subtype === "api_retry") {
124
+ apiRetryError = message.error;
125
+ }
102
126
  if (message.type === "assistant") {
127
+ // Capture the assistant-level error field (#732) — `rate_limit`,
128
+ // `billing_error`, `overloaded`, etc. Previously discarded by the
129
+ // text-only content filter below.
130
+ if (message.error) {
131
+ assistantError = message.error;
132
+ }
103
133
  const content = message.message.content;
104
134
  const textContent = content
105
135
  .filter((c) => c.type === "text" && c.text)
@@ -124,6 +154,10 @@ export class ClaudeCodeDriver {
124
154
  // `config.cwd`. `sessionId` is mirrored for one release (#674) so
125
155
  // upgraded callers can still drive resume off the deprecated field.
126
156
  const resumeHandle = this.buildResumeHandle(resultSessionId, config.cwd);
157
+ // Build a typed error from structured SDK signals (#732). Present only
158
+ // when the stream surfaced a rate-limit/billing failure; otherwise
159
+ // undefined and the executor falls back to stderr-regex classification.
160
+ const structuredError = this.buildStructuredError(rateLimitInfo, assistantError, apiRetryError);
127
161
  if (resultMessage) {
128
162
  if (resultMessage.subtype === "success") {
129
163
  return {
@@ -135,13 +169,29 @@ export class ClaudeCodeDriver {
135
169
  stdoutTail: stdoutBuffer.getLines(),
136
170
  };
137
171
  }
172
+ // Turn-cap is a soft, recoverable outcome, not a hard failure: the
173
+ // agent produced partial work before hitting its `maxTurns` ceiling
174
+ // (turn caps are live on every agent since #484). Warn (not error)
175
+ // and return the partial output flagged `capped` so consumers — the
176
+ // /qa and /exec skills — can treat it as inconclusive/incomplete
177
+ // rather than discarding the work. See #733. Branched out of the
178
+ // error switch below so it never carries a hard `error` string.
179
+ if (resultMessage.subtype === "error_max_turns") {
180
+ config.onStderr?.("⚠️ Agent hit its turn cap (error_max_turns). Returning partial results.\n");
181
+ return {
182
+ success: false,
183
+ capped: true,
184
+ output: capturedOutput,
185
+ sessionId: resultSessionId,
186
+ resumeHandle,
187
+ stderrTail: stderrBuffer.getLines(),
188
+ stdoutTail: stdoutBuffer.getLines(),
189
+ };
190
+ }
138
191
  // Handle error subtypes
139
192
  let error;
140
193
  const errorSubtype = resultMessage.subtype;
141
- if (errorSubtype === "error_max_turns") {
142
- error = "Max turns reached";
143
- }
144
- else if (errorSubtype === "error_during_execution") {
194
+ if (errorSubtype === "error_during_execution") {
145
195
  error = resultMessage.errors?.join(", ") || "Error during execution";
146
196
  }
147
197
  else if (errorSubtype === "error_max_budget_usd") {
@@ -155,7 +205,10 @@ export class ClaudeCodeDriver {
155
205
  output: capturedOutput,
156
206
  sessionId: resultSessionId,
157
207
  resumeHandle,
158
- error,
208
+ // Prefer the structured cause (e.g. "Out of credits") over the
209
+ // generic subtype text when available (#732).
210
+ error: structuredError?.message ?? error,
211
+ structuredError,
159
212
  stderrTail: stderrBuffer.getLines(),
160
213
  stdoutTail: stdoutBuffer.getLines(),
161
214
  };
@@ -165,7 +218,8 @@ export class ClaudeCodeDriver {
165
218
  output: capturedOutput,
166
219
  sessionId: resultSessionId,
167
220
  resumeHandle,
168
- error: "No result received from Claude",
221
+ error: structuredError?.message ?? "No result received from Claude",
222
+ structuredError,
169
223
  stderrTail: stderrBuffer.getLines(),
170
224
  stdoutTail: stdoutBuffer.getLines(),
171
225
  };
@@ -174,14 +228,31 @@ export class ClaudeCodeDriver {
174
228
  clearTimeout(timeoutId);
175
229
  const error = err instanceof Error ? err.message : String(err);
176
230
  if (error.includes("abort") || error.includes("AbortError")) {
231
+ // A rate limit can manifest as a hang rather than a stream event
232
+ // reaching the result: the SDK stalls, the phase timeout fires, and
233
+ // the abort lands here. Only failure-grade signals are ever captured
234
+ // (see the stream loop), so attaching them cannot mask a genuine
235
+ // timeout — with no signal, `abortStructuredError` is undefined and
236
+ // the bare timeout message survives unchanged (#761 AC-1). Without
237
+ // this, the captured `rateLimitInfo` was discarded and downstream
238
+ // classification saw only `Timeout after Ns`, sending a closed
239
+ // rate-limit window into the full retry + MCP-fallback ladder.
240
+ const abortStructuredError = this.buildStructuredError(rateLimitInfo, assistantError, apiRetryError);
177
241
  return {
178
242
  success: false,
179
243
  output: capturedOutput,
180
- error: `Timeout after ${config.phaseTimeout}s`,
244
+ error: abortStructuredError?.message ??
245
+ `Timeout after ${config.phaseTimeout}s`,
246
+ structuredError: abortStructuredError,
181
247
  stderrTail: stderrBuffer.getLines(),
182
248
  stdoutTail: stdoutBuffer.getLines(),
183
249
  };
184
250
  }
251
+ // If the stream surfaced a failure-grade rate-limit/billing signal before
252
+ // throwing, prefer that typed cause (#732) over the raw thrown message — a
253
+ // mid-stream throw after a *rejected* rate_limit_event is very likely the
254
+ // proximate cause.
255
+ const structuredError = this.buildStructuredError(rateLimitInfo, assistantError, apiRetryError);
185
256
  const stderrSuffix = capturedStderr
186
257
  ? `\nStderr: ${capturedStderr.slice(0, 500)}`
187
258
  : "";
@@ -190,12 +261,69 @@ export class ClaudeCodeDriver {
190
261
  output: capturedOutput,
191
262
  sessionId: resultSessionId,
192
263
  resumeHandle: this.buildResumeHandle(resultSessionId, config.cwd),
193
- error: error + stderrSuffix,
264
+ error: structuredError?.message ?? error + stderrSuffix,
265
+ structuredError,
194
266
  stderrTail: stderrBuffer.getLines(),
195
267
  stdoutTail: stdoutBuffer.getLines(),
196
268
  };
197
269
  }
198
270
  }
271
+ /**
272
+ * Derive a typed {@link SequantError} from structured SDK failure signals
273
+ * (#732). Precedence: a captured `rate_limit_event` (richest signal) wins;
274
+ * otherwise the assistant-level `error`; otherwise the last `api_retry`
275
+ * error. Returns undefined when no rate-limit/billing signal was seen, so
276
+ * the executor falls back to stderr-regex classification.
277
+ *
278
+ * Exception: a non-retryable billing failure must never be downgraded to a
279
+ * retryable {@link RateLimitError}. If the `rate_limit_event` was only a
280
+ * transient throttle but the assistant separately reported `billing_error`,
281
+ * the billing cause wins — a retry cannot refill credits, and a
282
+ * RateLimitError would wrongly re-enable the retry / MCP-fallback path. When
283
+ * the `rate_limit_event` is itself a billing failure its richer metadata
284
+ * (`canUserPurchaseCredits`, etc.) is preserved.
285
+ */
286
+ buildStructuredError(rateLimitInfo, assistantError, apiRetryError) {
287
+ if (rateLimitInfo) {
288
+ const err = createRateLimitError(rateLimitInfo);
289
+ if (err instanceof RateLimitError && assistantError === "billing_error") {
290
+ return new BillingError("Billing error");
291
+ }
292
+ return err;
293
+ }
294
+ return (this.errorFromAssistantError(assistantError) ??
295
+ this.errorFromAssistantError(apiRetryError));
296
+ }
297
+ /**
298
+ * Map the SDK's assistant/api-retry error enum to a typed error. Only
299
+ * rate-limit / billing variants are mapped; other variants (auth, etc.)
300
+ * return undefined and defer to the existing classification path.
301
+ *
302
+ * The assistant-error channel carries no `resetsAt`/`rateLimitType` (the
303
+ * enum is the whole signal — `SDKAssistantMessageError` is a bare string),
304
+ * so the originating variant is recorded in metadata to keep `rate_limit`
305
+ * and `overloaded` distinguishable downstream (#761 AC-9). With no
306
+ * `resetsAt`, these errors are treated as transient by the executor's
307
+ * window-exhaustion check — the mandatory metadata-absent fallback.
308
+ */
309
+ errorFromAssistantError(error) {
310
+ switch (error) {
311
+ case "billing_error":
312
+ return new BillingError("Billing error", {
313
+ assistantError: "billing_error",
314
+ });
315
+ case "rate_limit":
316
+ return new RateLimitError("Rate limited", {
317
+ assistantError: "rate_limit",
318
+ });
319
+ case "overloaded":
320
+ return new RateLimitError("API overloaded", {
321
+ assistantError: "overloaded",
322
+ });
323
+ default:
324
+ return undefined;
325
+ }
326
+ }
199
327
  buildResumeHandle(token, originCwd) {
200
328
  if (!token)
201
329
  return undefined;
@@ -5,8 +5,15 @@
5
5
  * categories. Exit codes are the primary signal; stderr patterns are secondary.
6
6
  */
7
7
  import { SequantError } from "../errors.js";
8
- /** All recognized error categories (kept for backwards compatibility). */
9
- export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "unknown"];
8
+ /**
9
+ * All recognized error categories (kept for backwards compatibility).
10
+ *
11
+ * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
+ * errors — `classifyError` never produces them, since stderr text cannot
13
+ * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
+ */
16
+ export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "unknown"];
10
17
  export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
11
18
  /**
12
19
  * Map from error type name to legacy category string.
@@ -5,13 +5,22 @@
5
5
  * categories. Exit codes are the primary signal; stderr patterns are secondary.
6
6
  */
7
7
  import { ContextOverflowError, ApiError, HookFailureError, BuildError, TimeoutError, SubprocessError, } from "../errors.js";
8
- /** All recognized error categories (kept for backwards compatibility). */
8
+ /**
9
+ * All recognized error categories (kept for backwards compatibility).
10
+ *
11
+ * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
+ * errors — `classifyError` never produces them, since stderr text cannot
13
+ * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
+ */
9
16
  export const ERROR_CATEGORIES = [
10
17
  "context_overflow",
11
18
  "api_error",
12
19
  "hook_failure",
13
20
  "build_error",
14
21
  "timeout",
22
+ "rate_limit",
23
+ "billing",
15
24
  "unknown",
16
25
  ];
17
26
  /**
@@ -30,6 +39,10 @@ export function errorTypeToCategory(error) {
30
39
  return "build_error";
31
40
  case "TimeoutError":
32
41
  return "timeout";
42
+ case "RateLimitError":
43
+ return "rate_limit";
44
+ case "BillingError":
45
+ return "billing";
33
46
  default:
34
47
  return "unknown";
35
48
  }
@@ -99,4 +99,4 @@ export declare class LogWriter {
99
99
  *
100
100
  * Utility function for creating phase logs when you have start/end times.
101
101
  */
102
- export declare function createPhaseLogFromTiming(phase: Phase, issueNumber: number, startTime: Date, endTime: Date, status: PhaseLog["status"], options?: Partial<Pick<PhaseLog, "error" | "iterations" | "filesModified" | "testsRun" | "testsPassed" | "verdict" | "summary" | "commitHash" | "fileDiffStats" | "cacheMetrics" | "errorContext">>): PhaseLog;
102
+ export declare function createPhaseLogFromTiming(phase: Phase, issueNumber: number, startTime: Date, endTime: Date, status: PhaseLog["status"], options?: Partial<Pick<PhaseLog, "error" | "capped" | "iterations" | "filesModified" | "testsRun" | "testsPassed" | "verdict" | "summary" | "commitHash" | "fileDiffStats" | "cacheMetrics" | "errorContext">>): PhaseLog;
@@ -16,7 +16,7 @@
16
16
  import * as fs from "fs";
17
17
  import * as path from "path";
18
18
  import * as os from "os";
19
- import { createEmptyRunLog, finalizeRunLog, generateLogFilename, LOG_PATHS, } from "./run-log-schema.js";
19
+ import { createEmptyRunLog, finalizeRunLog, deriveIssueLogStatus, generateLogFilename, LOG_PATHS, } from "./run-log-schema.js";
20
20
  import { rotateIfNeeded, DEFAULT_ROTATION_SETTINGS, } from "./log-rotation.js";
21
21
  /**
22
22
  * Manages writing structured run logs to disk
@@ -94,13 +94,11 @@ export class LogWriter {
94
94
  throw new Error(`No active issue #${phaseLog.issueNumber}. Call startIssue() first.`);
95
95
  }
96
96
  issue.phases = [...(issue.phases ?? []), phaseLog];
97
- // Update issue status based on phase result
98
- if (phaseLog.status === "failure") {
99
- issue.status = "failure";
100
- }
101
- else if (phaseLog.status === "timeout" && issue.status !== "failure") {
102
- issue.status = "partial";
103
- }
97
+ // #766: derive from the latest attempt of each phase (loop excluded) rather
98
+ // than pinning failure/partial forever. A timeout or failure that a later
99
+ // quality-loop iteration recovers from no longer sticks, so the JSON log
100
+ // agrees with the live card and summary table (AC-3/AC-5).
101
+ issue.status = deriveIssueLogStatus(issue.phases);
104
102
  if (this.verbose) {
105
103
  console.log(`Log phase: ${phaseLog.phase} (${phaseLog.status}) - ${phaseLog.durationSeconds.toFixed(1)}s`);
106
104
  }
@@ -25,6 +25,24 @@ export declare const RunOutcomeSchema: z.ZodEnum<{
25
25
  failed: "failed";
26
26
  }>;
27
27
  export type RunOutcome = z.infer<typeof RunOutcomeSchema>;
28
+ /**
29
+ * Bounded-enum classification of the failure that ended a run (#761 AC-7).
30
+ *
31
+ * Sourced from `ERROR_CATEGORIES` so the metric can never carry free text —
32
+ * error *messages* stay excluded per the privacy contract above MetricRunSchema
33
+ * (they could contain sensitive info); a closed enum cannot.
34
+ */
35
+ export declare const FailureCategorySchema: z.ZodEnum<{
36
+ unknown: "unknown";
37
+ timeout: "timeout";
38
+ context_overflow: "context_overflow";
39
+ api_error: "api_error";
40
+ hook_failure: "hook_failure";
41
+ build_error: "build_error";
42
+ rate_limit: "rate_limit";
43
+ billing: "billing";
44
+ }>;
45
+ export type FailureCategory = z.infer<typeof FailureCategorySchema>;
28
46
  /**
29
47
  * Available phases (aligned with run-log-schema.ts)
30
48
  */
@@ -83,6 +101,16 @@ export declare const MetricRunSchema: z.ZodObject<{
83
101
  duration: z.ZodNumber;
84
102
  model: z.ZodString;
85
103
  flags: z.ZodArray<z.ZodString>;
104
+ failureCategory: z.ZodOptional<z.ZodEnum<{
105
+ unknown: "unknown";
106
+ timeout: "timeout";
107
+ context_overflow: "context_overflow";
108
+ api_error: "api_error";
109
+ hook_failure: "hook_failure";
110
+ build_error: "build_error";
111
+ rate_limit: "rate_limit";
112
+ billing: "billing";
113
+ }>>;
86
114
  metrics: z.ZodObject<{
87
115
  tokensUsed: z.ZodNumber;
88
116
  filesChanged: z.ZodNumber;
@@ -123,6 +151,16 @@ export declare const MetricsSchema: z.ZodObject<{
123
151
  duration: z.ZodNumber;
124
152
  model: z.ZodString;
125
153
  flags: z.ZodArray<z.ZodString>;
154
+ failureCategory: z.ZodOptional<z.ZodEnum<{
155
+ unknown: "unknown";
156
+ timeout: "timeout";
157
+ context_overflow: "context_overflow";
158
+ api_error: "api_error";
159
+ hook_failure: "hook_failure";
160
+ build_error: "build_error";
161
+ rate_limit: "rate_limit";
162
+ billing: "billing";
163
+ }>>;
126
164
  metrics: z.ZodObject<{
127
165
  tokensUsed: z.ZodNumber;
128
166
  filesChanged: z.ZodNumber;
@@ -154,6 +192,7 @@ export declare function createMetricRun(options: {
154
192
  duration: number;
155
193
  model?: string;
156
194
  flags?: string[];
195
+ failureCategory?: FailureCategory;
157
196
  metrics?: Partial<RunMetrics>;
158
197
  }): MetricRun;
159
198
  /**
@@ -17,10 +17,19 @@
17
17
  */
18
18
  import { randomUUID } from "node:crypto";
19
19
  import { z } from "zod";
20
+ import { ERROR_CATEGORIES } from "./error-classifier.js";
20
21
  /**
21
22
  * Outcome of a workflow run
22
23
  */
23
24
  export const RunOutcomeSchema = z.enum(["success", "partial", "failed"]);
25
+ /**
26
+ * Bounded-enum classification of the failure that ended a run (#761 AC-7).
27
+ *
28
+ * Sourced from `ERROR_CATEGORIES` so the metric can never carry free text —
29
+ * error *messages* stay excluded per the privacy contract above MetricRunSchema
30
+ * (they could contain sensitive info); a closed enum cannot.
31
+ */
32
+ export const FailureCategorySchema = z.enum(ERROR_CATEGORIES);
24
33
  /**
25
34
  * Available phases (aligned with run-log-schema.ts)
26
35
  */
@@ -81,6 +90,12 @@ export const MetricRunSchema = z.object({
81
90
  model: z.string(),
82
91
  /** CLI flags used (e.g., ["--chain", "--sequential"]) */
83
92
  flags: z.array(z.string()),
93
+ /**
94
+ * Category of the failure that ended the run (#761 AC-7). Optional and
95
+ * enum-only; absent on success and on records written before this field
96
+ * existed (additive — no `version` bump required).
97
+ */
98
+ failureCategory: FailureCategorySchema.optional(),
84
99
  /** Aggregate metrics */
85
100
  metrics: RunMetricsSchema,
86
101
  });
@@ -121,6 +136,7 @@ export function createMetricRun(options) {
121
136
  duration: options.duration,
122
137
  model: options.model ?? "unknown",
123
138
  flags: options.flags ?? [],
139
+ failureCategory: options.failureCategory,
124
140
  metrics: {
125
141
  tokensUsed: options.metrics?.tokensUsed ?? 0,
126
142
  filesChanged: options.metrics?.filesChanged ?? 0,
@@ -20,7 +20,7 @@
20
20
  * });
21
21
  * ```
22
22
  */
23
- import { type Metrics, type MetricRun, type MetricPhase, type RunOutcome, type RunMetrics } from "./metrics-schema.js";
23
+ import { type Metrics, type MetricRun, type MetricPhase, type RunOutcome, type RunMetrics, type FailureCategory } from "./metrics-schema.js";
24
24
  export interface MetricsWriterOptions {
25
25
  /** Path to metrics file (default: .sequant/metrics.json) */
26
26
  metricsPath?: string;
@@ -69,6 +69,7 @@ export declare class MetricsWriter {
69
69
  duration: number;
70
70
  model?: string;
71
71
  flags?: string[];
72
+ failureCategory?: FailureCategory;
72
73
  metrics?: Partial<RunMetrics>;
73
74
  }): Promise<MetricRun>;
74
75
  /**
@@ -11,6 +11,7 @@ import { ShutdownManager } from "../shutdown.js";
11
11
  import { Phase, ExecutionConfig, PhaseResult, QaVerdict, PhasePauseHandle } from "./types.js";
12
12
  import type { QaSummary } from "./run-log-schema.js";
13
13
  import type { AgentPhaseResult, ResumeHandle } from "./drivers/index.js";
14
+ import { SequantError } from "../errors.js";
14
15
  /**
15
16
  * Leading + trailing throttle. Fires the wrapped callback immediately on the
16
17
  * first call, drops subsequent calls that arrive inside `intervalMs` but
@@ -35,6 +36,37 @@ export declare function createThrottledReporter(fn: (text: string) => void, inte
35
36
  export declare const SPEC_RETRY_BACKOFF_MS: number;
36
37
  /** @internal Exported for testing only */
37
38
  export declare const SPEC_EXTRA_RETRIES: number;
39
+ /**
40
+ * A rate limit whose window resets further out than this is treated as
41
+ * exhausted rather than transient (#761 AC-2): no retry can succeed inside a
42
+ * closed window, so consuming cold-start retries (each burning up to a full
43
+ * `phaseTimeout`) only delays the labeled halt. Five minutes comfortably
44
+ * exceeds any backoff this executor performs while staying far below the
45
+ * five-hour/seven-day windows the check exists to catch.
46
+ *
47
+ * @internal Exported for testing only
48
+ */
49
+ export declare const RATE_LIMIT_WINDOW_SKIP_THRESHOLD_MS: number;
50
+ /**
51
+ * Base backoff for transient rate-limit retries (#761 AC-4), doubled per
52
+ * attempt (5s, 10s). Same scale as `SPEC_RETRY_BACKOFF_MS` — long enough to
53
+ * outlive a momentary throttle, short enough to be negligible next to a
54
+ * phase's runtime.
55
+ *
56
+ * @internal Exported for testing only
57
+ */
58
+ export declare const RATE_LIMIT_RETRY_BACKOFF_MS = 5000;
59
+ /**
60
+ * True when a failure is a rate limit whose reset lies beyond
61
+ * {@link RATE_LIMIT_WINDOW_SKIP_THRESHOLD_MS} — i.e. window exhaustion, not a
62
+ * transient throttle. Metadata-absent rate limits (the assistant-error channel
63
+ * carries no `resetsAt`, see #761 AC-9) return false and fall through to the
64
+ * transient path: with no timing signal, retry-with-backoff is the safe
65
+ * default, skipping all retries is not.
66
+ *
67
+ * @internal Exported for testing only
68
+ */
69
+ export declare function isWindowExhaustedRateLimit(error: SequantError | undefined, now?: number): boolean;
38
70
  export declare function parseQaVerdict(output: string): QaVerdict | null;
39
71
  /**
40
72
  * Parse condensed QA summary from QA phase output (#434).
@@ -111,6 +143,24 @@ export declare function mapAgentSuccessToPhaseResult(phase: Phase, agentResult:
111
143
  sessionId?: string;
112
144
  resumeHandle?: ResumeHandle;
113
145
  };
146
+ /**
147
+ * Map a failed driver result to a `PhaseResult`.
148
+ *
149
+ * Symmetric to {@link mapAgentSuccessToPhaseResult}; extracted so the
150
+ * failure-path mapping (notably the #739 capped/output gating) is unit-testable
151
+ * without spawning a driver.
152
+ *
153
+ * `output` is propagated **only** for a capped phase (#739): a capped result is
154
+ * incomplete-but-not-hard-failed, so its partial work must survive downstream.
155
+ * A genuine (non-capped) failure keeps the historical behaviour of dropping
156
+ * `output`, leaving the `/loop` fix-context (`formatFailureContext`) unchanged.
157
+ *
158
+ * @internal Exported for testing only
159
+ */
160
+ export declare function mapAgentFailureToPhaseResult(phase: Phase, agentResult: AgentPhaseResult, durationSeconds: number): PhaseResult & {
161
+ sessionId?: string;
162
+ resumeHandle?: ResumeHandle;
163
+ };
114
164
  /**
115
165
  * Get the prompt for a phase with the issue number substituted.
116
166
  * Selects self-contained prompts for non-Claude agents.