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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -2
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +12 -2
- package/dist/bin/preflight.d.ts +21 -0
- package/dist/bin/preflight.js +45 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +2 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/force-push.md +34 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +117 -19
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +29 -0
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +100 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +24 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +285 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +202 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +287 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/test-quality-checklist.md +272 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +40 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +95 -11
- package/dist/marketplace/external_plugins/sequant/skills/references/shared/framework-gotchas.md +186 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +669 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/references/browser-testing-patterns.md +423 -0
- package/dist/marketplace/external_plugins/sequant/skills/upstream/SKILL.md +419 -0
- package/dist/src/commands/logs.js +6 -1
- package/dist/src/commands/run-display.d.ts +20 -0
- package/dist/src/commands/run-display.js +80 -1
- package/dist/src/commands/stats.js +47 -0
- package/dist/src/lib/assess-collision-detect.d.ts +19 -2
- package/dist/src/lib/assess-collision-detect.js +68 -4
- package/dist/src/lib/cli-ui/run-renderer.js +17 -9
- package/dist/src/lib/errors.d.ts +91 -0
- package/dist/src/lib/errors.js +118 -0
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/version-check.d.ts +19 -0
- package/dist/src/lib/version-check.js +45 -5
- package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
- package/dist/src/lib/workflow/batch-executor.js +142 -24
- package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/chain-preflight.js +199 -0
- package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
- package/dist/src/lib/workflow/chain-resume.js +166 -0
- package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
- package/dist/src/lib/workflow/dependency-markers.js +79 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +29 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +136 -8
- package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
- package/dist/src/lib/workflow/error-classifier.js +14 -1
- package/dist/src/lib/workflow/log-writer.d.ts +1 -1
- package/dist/src/lib/workflow/log-writer.js +6 -8
- package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
- package/dist/src/lib/workflow/metrics-schema.js +16 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +50 -0
- package/dist/src/lib/workflow/phase-executor.js +151 -17
- package/dist/src/lib/workflow/run-log-schema.d.ts +26 -0
- package/dist/src/lib/workflow/run-log-schema.js +52 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
- package/dist/src/lib/workflow/run-orchestrator.js +291 -30
- package/dist/src/lib/workflow/state-manager.d.ts +1 -0
- package/dist/src/lib/workflow/state-manager.js +6 -0
- package/dist/src/lib/workflow/state-schema.d.ts +3 -0
- package/dist/src/lib/workflow/state-schema.js +7 -0
- package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
- package/dist/src/lib/workflow/status-derivation.js +27 -0
- package/dist/src/lib/workflow/types.d.ts +40 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
- package/dist/src/lib/workflow/worktree-manager.js +103 -33
- package/dist/src/mcp/tools/run.d.ts +2 -0
- package/dist/src/mcp/tools/run.js +2 -0
- package/dist/src/ui/tui/theme.d.ts +18 -4
- package/dist/src/ui/tui/theme.js +18 -4
- package/package.json +5 -6
- package/templates/hooks/post-tool.sh +18 -3
- package/templates/hooks/pre-tool.sh +330 -57
- package/templates/scripts/cleanup-worktree.sh +103 -14
- package/templates/skills/_shared/references/force-push.md +34 -0
- package/templates/skills/assess/SKILL.md +117 -19
- package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
- package/templates/skills/exec/SKILL.md +29 -0
- package/templates/skills/fullsolve/SKILL.md +1 -1
- package/templates/skills/loop/SKILL.md +100 -2
- package/templates/skills/qa/SKILL.md +24 -0
- package/templates/skills/qa/references/anti-pattern-detection.md +285 -0
- package/templates/skills/qa/references/call-site-review.md +202 -0
- package/templates/skills/qa/references/quality-gates.md +287 -0
- package/templates/skills/qa/references/test-quality-checklist.md +272 -0
- package/templates/skills/qa/references/testing-requirements.md +40 -0
- package/templates/skills/qa/scripts/quality-checks.sh +95 -11
- package/templates/skills/references/shared/framework-gotchas.md +186 -0
- package/templates/skills/reflect/SKILL.md +27 -13
- package/templates/skills/reflect/references/documentation-tiers.md +80 -68
- package/templates/skills/reflect/references/phase-reflection.md +31 -15
- package/templates/skills/release/SKILL.md +669 -0
- package/templates/skills/spec/references/verification-criteria.md +1 -1
- package/templates/skills/test/references/browser-testing-patterns.md +423 -0
- 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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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") */
|