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
@@ -12,7 +12,7 @@ import { execSync, execFileSync } from "child_process";
12
12
  import { readAgentsMd } from "../agents-md.js";
13
13
  import { getDriver } from "./drivers/index.js";
14
14
  import { classifyError } from "./error-classifier.js";
15
- import { ApiError } from "../errors.js";
15
+ import { ApiError, BillingError, RateLimitError, resetsAtToMs, } from "../errors.js";
16
16
  import { phaseRegistry } from "./phase-registry.js";
17
17
  import { bracketedConsoleLog } from "./notice.js";
18
18
  /**
@@ -98,6 +98,44 @@ const SPEC_RETRY_STRATEGY = phaseRegistry.get("spec").retryStrategy;
98
98
  export const SPEC_RETRY_BACKOFF_MS = SPEC_RETRY_STRATEGY?.backoffMs ?? 5000;
99
99
  /** @internal Exported for testing only */
100
100
  export const SPEC_EXTRA_RETRIES = SPEC_RETRY_STRATEGY?.extraRetries ?? 1;
101
+ /**
102
+ * A rate limit whose window resets further out than this is treated as
103
+ * exhausted rather than transient (#761 AC-2): no retry can succeed inside a
104
+ * closed window, so consuming cold-start retries (each burning up to a full
105
+ * `phaseTimeout`) only delays the labeled halt. Five minutes comfortably
106
+ * exceeds any backoff this executor performs while staying far below the
107
+ * five-hour/seven-day windows the check exists to catch.
108
+ *
109
+ * @internal Exported for testing only
110
+ */
111
+ export const RATE_LIMIT_WINDOW_SKIP_THRESHOLD_MS = 5 * 60 * 1000;
112
+ /**
113
+ * Base backoff for transient rate-limit retries (#761 AC-4), doubled per
114
+ * attempt (5s, 10s). Same scale as `SPEC_RETRY_BACKOFF_MS` — long enough to
115
+ * outlive a momentary throttle, short enough to be negligible next to a
116
+ * phase's runtime.
117
+ *
118
+ * @internal Exported for testing only
119
+ */
120
+ export const RATE_LIMIT_RETRY_BACKOFF_MS = 5000;
121
+ /**
122
+ * True when a failure is a rate limit whose reset lies beyond
123
+ * {@link RATE_LIMIT_WINDOW_SKIP_THRESHOLD_MS} — i.e. window exhaustion, not a
124
+ * transient throttle. Metadata-absent rate limits (the assistant-error channel
125
+ * carries no `resetsAt`, see #761 AC-9) return false and fall through to the
126
+ * transient path: with no timing signal, retry-with-backoff is the safe
127
+ * default, skipping all retries is not.
128
+ *
129
+ * @internal Exported for testing only
130
+ */
131
+ export function isWindowExhaustedRateLimit(error, now = Date.now()) {
132
+ if (!(error instanceof RateLimitError))
133
+ return false;
134
+ const resetsAt = error.metadata.resetsAt;
135
+ if (typeof resetsAt !== "number")
136
+ return false;
137
+ return resetsAtToMs(resetsAt) - now > RATE_LIMIT_WINDOW_SKIP_THRESHOLD_MS;
138
+ }
101
139
  export function parseQaVerdict(output) {
102
140
  if (!output)
103
141
  return null;
@@ -348,9 +386,12 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
348
386
  const summary = agentResult.output
349
387
  ? (parseQaSummary(agentResult.output) ?? undefined)
350
388
  : undefined;
351
- if (verdict &&
352
- verdict !== "READY_FOR_MERGE" &&
353
- verdict !== "NEEDS_VERIFICATION") {
389
+ if (verdict === "AC_NOT_MET") {
390
+ // #749: only AC_NOT_MET (and the null branch below, #534) hard-fails.
391
+ // AC_MET_BUT_NOT_A_PLUS is a stopping/ready state — it must break to PR,
392
+ // not feed the quality loop (mirrors ready-gate.ts's `ac` policy). The
393
+ // verdict is retained on the success result so the PR/log surfaces the
394
+ // "not A+" note.
354
395
  return {
355
396
  phase,
356
397
  success: false,
@@ -408,6 +449,43 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
408
449
  ...tails,
409
450
  };
410
451
  }
452
+ /**
453
+ * Map a failed driver result to a `PhaseResult`.
454
+ *
455
+ * Symmetric to {@link mapAgentSuccessToPhaseResult}; extracted so the
456
+ * failure-path mapping (notably the #739 capped/output gating) is unit-testable
457
+ * without spawning a driver.
458
+ *
459
+ * `output` is propagated **only** for a capped phase (#739): a capped result is
460
+ * incomplete-but-not-hard-failed, so its partial work must survive downstream.
461
+ * A genuine (non-capped) failure keeps the historical behaviour of dropping
462
+ * `output`, leaving the `/loop` fix-context (`formatFailureContext`) unchanged.
463
+ *
464
+ * @internal Exported for testing only
465
+ */
466
+ export function mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds) {
467
+ return {
468
+ phase,
469
+ success: false,
470
+ durationSeconds,
471
+ error: agentResult.error,
472
+ // Propagate the driver's typed cause (#732) so the retry logic can prefer
473
+ // it over stderr-regex classification and gate the MCP fallback.
474
+ structuredError: agentResult.structuredError,
475
+ // Propagate the turn-cap flag and the partial output (#739). On the failure
476
+ // path `output` was previously dropped entirely — for a capped phase the
477
+ // partial work is usable and must be preserved, mirroring the driver/skill
478
+ // slice from #733. Gating `output` on `capped` keeps non-capped failures
479
+ // byte-for-byte identical to pre-#739 behaviour.
480
+ capped: agentResult.capped,
481
+ output: agentResult.capped ? agentResult.output : undefined,
482
+ sessionId: agentResult.sessionId,
483
+ resumeHandle: agentResult.resumeHandle,
484
+ stderrTail: agentResult.stderrTail,
485
+ stdoutTail: agentResult.stdoutTail,
486
+ exitCode: agentResult.exitCode,
487
+ };
488
+ }
411
489
  /**
412
490
  * Get the prompt for a phase with the issue number substituted.
413
491
  * Selects self-contained prompts for non-Claude agents.
@@ -642,17 +720,7 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
642
720
  if (agentResult.success) {
643
721
  return mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
644
722
  }
645
- return {
646
- phase,
647
- success: false,
648
- durationSeconds,
649
- error: agentResult.error,
650
- sessionId: agentResult.sessionId,
651
- resumeHandle: agentResult.resumeHandle,
652
- stderrTail: agentResult.stderrTail,
653
- stdoutTail: agentResult.stdoutTail,
654
- exitCode: agentResult.exitCode,
655
- };
723
+ return mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds);
656
724
  }
657
725
  /**
658
726
  * Execute a phase with automatic retry for cold-start failures and MCP fallback.
@@ -693,6 +761,14 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
693
761
  if (lastResult.success) {
694
762
  return lastResult;
695
763
  }
764
+ // Turn-capped phase (#739): incomplete-but-not-hard-failed. A retry cannot
765
+ // un-cap a turn limit, so short-circuit before any fallback — same rationale
766
+ // as the billing skip (#732), but capped must skip *all* retries (incl.
767
+ // cold-start), so an explicit early return is required, not just a guard
768
+ // flag at the MCP gate.
769
+ if (lastResult.capped) {
770
+ return lastResult;
771
+ }
696
772
  }
697
773
  else {
698
774
  // Phase 1: Cold-start retry attempts (with MCP enabled if configured)
@@ -703,11 +779,49 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
703
779
  if (lastResult.success) {
704
780
  return lastResult;
705
781
  }
782
+ // Turn-capped phase (#739): short-circuit before cold-start retries, the
783
+ // MCP fallback, and the spec-extra retry — a retry cannot un-cap a turn
784
+ // limit. The early return here (rather than a guard at the MCP gate alone)
785
+ // is what skips the cold-start re-spawns, unlike the billing case which
786
+ // still cold-start-retries in the <60s window.
787
+ if (lastResult.capped) {
788
+ return lastResult;
789
+ }
790
+ // Window-exhausted rate limit (#761 AC-2): the reset is hours away, so
791
+ // every retry re-spawns into the same closed window — worst case
792
+ // ~4 × phaseTimeout (≈2h) of doomed attempts before the run halts.
793
+ // Modelled on the `capped` early return above: skip all remaining
794
+ // cold-start retries and (via the return) the MCP fallback. Checked
795
+ // before the duration branch because a rate-limit rejection typically
796
+ // fails fast and would otherwise be mistaken for a cold-start failure.
797
+ if (isWindowExhaustedRateLimit(lastResult.structuredError)) {
798
+ if (config.verbose) {
799
+ bracketedConsoleLog(spinner, chalk.yellow(`\n ✕ ${lastResult.error ?? "Rate limited"} — window exhausted, skipping retries`));
800
+ }
801
+ return lastResult;
802
+ }
803
+ // Transient rate limit (#761 AC-4): retry, but with real backoff — the
804
+ // bare `continue` this replaces re-spawned immediately into the same
805
+ // throttle. Reuses the injected `delayFn`; delay doubles per attempt.
806
+ // Metadata-absent rate limits land here by design (AC-9 fallback rule).
807
+ if (lastResult.structuredError instanceof RateLimitError &&
808
+ attempt < COLD_START_MAX_RETRIES) {
809
+ const backoffMs = RATE_LIMIT_RETRY_BACKOFF_MS * 2 ** attempt;
810
+ if (config.verbose) {
811
+ bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ ${lastResult.error ?? "Rate limited"} — backing off ${backoffMs}ms before retry... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
812
+ }
813
+ await delayFn(backoffMs);
814
+ continue;
815
+ }
706
816
  // Genuine failure (took long enough to be real work) → skip cold-start retries.
707
817
  // Use error classification (AC-9): if the error is retryable (e.g., API
708
818
  // rate limit, transient 503), allow one more attempt even for genuine failures.
709
819
  if (duration >= COLD_START_THRESHOLD_SECONDS) {
710
- const typedError = classifyError(lastResult.stderrTail ?? [], lastResult.exitCode);
820
+ // Prefer the driver's structured cause (#732) it reflects the real
821
+ // SDK rate-limit/billing signal — over stderr-regex classification,
822
+ // which only sees text and never the structured data.
823
+ const typedError = lastResult.structuredError ??
824
+ classifyError(lastResult.stderrTail ?? [], lastResult.exitCode);
711
825
  if (typedError.isRetryable && attempt < COLD_START_MAX_RETRIES) {
712
826
  if (config.verbose) {
713
827
  const label = typedError instanceof ApiError
@@ -735,7 +849,27 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
735
849
  // Phase 2: MCP fallback - if MCP is enabled and we're still failing, try without MCP
736
850
  // This handles npx-based MCP servers that fail on first run due to cold-cache issues.
737
851
  // Skip for `loop` phase — MCP is never the cause of loop failures (#488).
738
- if (config.mcp && !lastResult.success && !skipColdStartRetry) {
852
+ //
853
+ // Also skip when the failure is a billing/credits error (#732): a no-MCP
854
+ // retry cannot refill credits, so the misleading "retrying without MCP"
855
+ // noise (#592) would only mask the real cause. The accurate structured
856
+ // message (e.g. "Out of credits") is surfaced instead.
857
+ const failureIsBilling = lastResult.structuredError instanceof BillingError;
858
+ // Belt-and-suspenders (#739): the capped early-returns above already exit
859
+ // before reaching here, but gate the MCP fallback on `!failureIsCapped` too so
860
+ // intent is documented and future code paths can't accidentally re-spawn a
861
+ // capped phase without MCP.
862
+ const failureIsCapped = lastResult.capped === true;
863
+ // A throttle must not trigger "retrying without MCP" (#761 AC-3): MCP was
864
+ // never the cause, and the re-spawn burns up to another full phaseTimeout
865
+ // against the same limit while mislabeling the failure as MCP-related.
866
+ const failureIsRateLimited = lastResult.structuredError instanceof RateLimitError;
867
+ if (config.mcp &&
868
+ !lastResult.success &&
869
+ !skipColdStartRetry &&
870
+ !failureIsBilling &&
871
+ !failureIsCapped &&
872
+ !failureIsRateLimited) {
739
873
  bracketedConsoleLog(spinner, chalk.yellow(`\n ! Phase failed with MCP enabled, retrying without MCP...`));
740
874
  // Create config copy with MCP disabled
741
875
  const configWithoutMcp = {
@@ -89,6 +89,8 @@ export declare const ErrorContextSchema: z.ZodObject<{
89
89
  api_error: "api_error";
90
90
  hook_failure: "hook_failure";
91
91
  build_error: "build_error";
92
+ rate_limit: "rate_limit";
93
+ billing: "billing";
92
94
  }>;
93
95
  errorType: z.ZodOptional<z.ZodString>;
94
96
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -125,6 +127,7 @@ export declare const PhaseLogSchema: z.ZodObject<{
125
127
  timeout: "timeout";
126
128
  }>;
127
129
  error: z.ZodOptional<z.ZodString>;
130
+ capped: z.ZodOptional<z.ZodBoolean>;
128
131
  iterations: z.ZodOptional<z.ZodNumber>;
129
132
  filesModified: z.ZodOptional<z.ZodArray<z.ZodString>>;
130
133
  testsRun: z.ZodOptional<z.ZodNumber>;
@@ -169,6 +172,8 @@ export declare const PhaseLogSchema: z.ZodObject<{
169
172
  api_error: "api_error";
170
173
  hook_failure: "hook_failure";
171
174
  build_error: "build_error";
175
+ rate_limit: "rate_limit";
176
+ billing: "billing";
172
177
  }>;
173
178
  errorType: z.ZodOptional<z.ZodString>;
174
179
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -201,6 +206,7 @@ export declare const IssueLogSchema: z.ZodObject<{
201
206
  timeout: "timeout";
202
207
  }>;
203
208
  error: z.ZodOptional<z.ZodString>;
209
+ capped: z.ZodOptional<z.ZodBoolean>;
204
210
  iterations: z.ZodOptional<z.ZodNumber>;
205
211
  filesModified: z.ZodOptional<z.ZodArray<z.ZodString>>;
206
212
  testsRun: z.ZodOptional<z.ZodNumber>;
@@ -245,6 +251,8 @@ export declare const IssueLogSchema: z.ZodObject<{
245
251
  api_error: "api_error";
246
252
  hook_failure: "hook_failure";
247
253
  build_error: "build_error";
254
+ rate_limit: "rate_limit";
255
+ billing: "billing";
248
256
  }>;
249
257
  errorType: z.ZodOptional<z.ZodString>;
250
258
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -275,6 +283,7 @@ export declare const RunSummarySchema: z.ZodObject<{
275
283
  totalIssues: z.ZodNumber;
276
284
  passed: z.ZodNumber;
277
285
  failed: z.ZodNumber;
286
+ partial: z.ZodDefault<z.ZodNumber>;
278
287
  totalDurationSeconds: z.ZodNumber;
279
288
  }, z.core.$strip>;
280
289
  export type RunSummary = z.infer<typeof RunSummarySchema>;
@@ -318,6 +327,7 @@ export declare const RunLogSchema: z.ZodObject<{
318
327
  timeout: "timeout";
319
328
  }>;
320
329
  error: z.ZodOptional<z.ZodString>;
330
+ capped: z.ZodOptional<z.ZodBoolean>;
321
331
  iterations: z.ZodOptional<z.ZodNumber>;
322
332
  filesModified: z.ZodOptional<z.ZodArray<z.ZodString>>;
323
333
  testsRun: z.ZodOptional<z.ZodNumber>;
@@ -362,6 +372,8 @@ export declare const RunLogSchema: z.ZodObject<{
362
372
  api_error: "api_error";
363
373
  hook_failure: "hook_failure";
364
374
  build_error: "build_error";
375
+ rate_limit: "rate_limit";
376
+ billing: "billing";
365
377
  }>;
366
378
  errorType: z.ZodOptional<z.ZodString>;
367
379
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -376,6 +388,7 @@ export declare const RunLogSchema: z.ZodObject<{
376
388
  totalIssues: z.ZodNumber;
377
389
  passed: z.ZodNumber;
378
390
  failed: z.ZodNumber;
391
+ partial: z.ZodDefault<z.ZodNumber>;
379
392
  totalDurationSeconds: z.ZodNumber;
380
393
  }, z.core.$strip>;
381
394
  startCommit: z.ZodOptional<z.ZodString>;
@@ -436,3 +449,16 @@ export declare function completePhaseLog(phaseLog: Omit<PhaseLog, "endTime" | "d
436
449
  export declare function finalizeRunLog(runLog: Omit<RunLog, "endTime">, options?: {
437
450
  endCommit?: string;
438
451
  }): RunLog;
452
+ /**
453
+ * Derive an issue's overall log status from its phase log entries (#766).
454
+ *
455
+ * Phases are appended in execution order, so the last entry for a given phase
456
+ * name is its latest attempt — that attempt wins. This lets a `timeout`/
457
+ * `failure` that a later quality-loop iteration recovers from de-escalate to
458
+ * `success`, keeping the JSON log consistent with the live card and summary
459
+ * table (AC-3/AC-5). `loop` is auxiliary recovery and never determines the
460
+ * verdict (mirrors the live-card rule); an unrecovered failure still leaves a
461
+ * non-loop phase failed. Priority among latest attempts: failure > timeout >
462
+ * success.
463
+ */
464
+ export declare function deriveIssueLogStatus(phases: PhaseLog[]): IssueStatus;
@@ -79,13 +79,19 @@ export const ErrorContextSchema = z.object({
79
79
  stdoutTail: z.array(z.string()),
80
80
  /** Process exit code */
81
81
  exitCode: z.number().int().optional(),
82
- /** Classified error category (legacy, kept for backwards compatibility) */
82
+ /**
83
+ * Classified error category (legacy, kept for backwards compatibility).
84
+ * Keep in sync with `ERROR_CATEGORIES` in `error-classifier.ts` —
85
+ * `rate_limit` / `billing` added by #761 AC-6.
86
+ */
83
87
  category: z.enum([
84
88
  "context_overflow",
85
89
  "api_error",
86
90
  "hook_failure",
87
91
  "build_error",
88
92
  "timeout",
93
+ "rate_limit",
94
+ "billing",
89
95
  "unknown",
90
96
  ]),
91
97
  /** Typed error class name (AC-8), e.g. "ApiError", "BuildError" */
@@ -130,6 +136,13 @@ export const PhaseLogSchema = z.object({
130
136
  status: PhaseStatusSchema,
131
137
  /** Error message if failed */
132
138
  error: z.string().optional(),
139
+ /**
140
+ * Set when the phase hit its turn cap (`error_max_turns`) (#739). Distinguishes
141
+ * an incomplete-but-not-hard-failed phase (partial output preserved) from a
142
+ * genuine failure. Reuses the `"failure"` status — additive boolean rather than
143
+ * a new `PhaseStatus` enum value, to keep the persisted-log schema stable.
144
+ */
145
+ capped: z.boolean().optional(),
133
146
  /** Number of iterations (for loop phase) */
134
147
  iterations: z.number().int().nonnegative().optional(),
135
148
  /** Files modified during this phase */
@@ -199,6 +212,13 @@ export const RunSummarySchema = z.object({
199
212
  passed: z.number().int().nonnegative(),
200
213
  /** Number of issues that failed */
201
214
  failed: z.number().int().nonnegative(),
215
+ /**
216
+ * Number of issues that ended `partial` — timed out with no genuine failure
217
+ * and no recovery (#766). Given its own bucket so an all-partial run no longer
218
+ * vanishes from both `passed` and `failed` (the `0 passed · 0 failed` bug).
219
+ * `.default(0)` keeps logs written before this field parseable.
220
+ */
221
+ partial: z.number().int().nonnegative().default(0),
202
222
  /** Total execution time in seconds */
203
223
  totalDurationSeconds: z.number().nonnegative(),
204
224
  });
@@ -267,6 +287,7 @@ export function createEmptyRunLog(config, options) {
267
287
  totalIssues: 0,
268
288
  passed: 0,
269
289
  failed: 0,
290
+ partial: 0,
270
291
  totalDurationSeconds: 0,
271
292
  },
272
293
  startCommit: options?.startCommit,
@@ -319,6 +340,9 @@ export function finalizeRunLog(runLog, options) {
319
340
  const totalDurationSeconds = (endTime.getTime() - startTime.getTime()) / 1000;
320
341
  const passed = runLog.issues.filter((i) => i.status === "success").length;
321
342
  const failed = runLog.issues.filter((i) => i.status === "failure").length;
343
+ // #766: `partial` gets its own bucket so an all-partial run isn't counted as
344
+ // `0 passed · 0 failed` — it landed in neither before.
345
+ const partial = runLog.issues.filter((i) => i.status === "partial").length;
322
346
  return {
323
347
  ...runLog,
324
348
  endTime: endTime.toISOString(),
@@ -326,8 +350,35 @@ export function finalizeRunLog(runLog, options) {
326
350
  totalIssues: runLog.issues.length,
327
351
  passed,
328
352
  failed,
353
+ partial,
329
354
  totalDurationSeconds,
330
355
  },
331
356
  endCommit: options?.endCommit ?? runLog.endCommit,
332
357
  };
333
358
  }
359
+ /**
360
+ * Derive an issue's overall log status from its phase log entries (#766).
361
+ *
362
+ * Phases are appended in execution order, so the last entry for a given phase
363
+ * name is its latest attempt — that attempt wins. This lets a `timeout`/
364
+ * `failure` that a later quality-loop iteration recovers from de-escalate to
365
+ * `success`, keeping the JSON log consistent with the live card and summary
366
+ * table (AC-3/AC-5). `loop` is auxiliary recovery and never determines the
367
+ * verdict (mirrors the live-card rule); an unrecovered failure still leaves a
368
+ * non-loop phase failed. Priority among latest attempts: failure > timeout >
369
+ * success.
370
+ */
371
+ export function deriveIssueLogStatus(phases) {
372
+ const latest = new Map();
373
+ for (const p of phases) {
374
+ if (p.phase === "loop")
375
+ continue;
376
+ latest.set(p.phase, p.status);
377
+ }
378
+ const statuses = [...latest.values()];
379
+ if (statuses.some((s) => s === "failure"))
380
+ return "failure";
381
+ if (statuses.some((s) => s === "timeout"))
382
+ return "partial";
383
+ return "success";
384
+ }
@@ -13,6 +13,7 @@ import { LogWriter } from "./log-writer.js";
13
13
  import { StateManager } from "./state-manager.js";
14
14
  import { ShutdownManager } from "../shutdown.js";
15
15
  import type { LockFile } from "../locks/index.js";
16
+ import { type ChainResumePlan } from "./chain-resume.js";
16
17
  import { WorkflowEventEmitter } from "./event-emitter.js";
17
18
  import type { SequantSettings } from "../settings.js";
18
19
  /**
@@ -55,6 +56,12 @@ export interface OrchestratorConfig {
55
56
  packageManager?: string;
56
57
  /** Base branch for rebase/PR targets */
57
58
  baseBranch?: string;
59
+ /**
60
+ * Chain resume plan (#760). Present only when re-running a `--chain` batch
61
+ * whose completed prefix is being skipped. Drives the first active link's
62
+ * rebase onto the last completed link's committed tip in `executeSequential`.
63
+ */
64
+ chainResume?: ChainResumePlan;
58
65
  /** Per-phase progress callback (parallel mode) */
59
66
  onProgress?: ProgressCallback;
60
67
  /** #672 AC-2: phase-plan callback forwarded into per-issue contexts. */
@@ -112,6 +119,13 @@ export interface ResolvedRun {
112
119
  config: ExecutionConfig;
113
120
  /** Parsed + dep-sorted issue numbers (pre-state-guard) */
114
121
  issueNumbers: number[];
122
+ /**
123
+ * Raw CLI issue order BEFORE `sortByDependencies` reorders it (#762).
124
+ * The chain content pre-flight must compare against this order — comparing
125
+ * against the dep-sorted `issueNumbers` would make the dependency-order and
126
+ * file-overlap-order checks dead code, since the sorter already fixed them.
127
+ */
128
+ rawIssueOrder: number[];
115
129
  /** Resolved batches if --batch specified, else null */
116
130
  batches: number[][] | null;
117
131
  /** Resolved base branch (CLI → settings → auto-detect → "main") */