sequant 2.8.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +9 -1
  4. package/dist/bin/cli.js +2 -1
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/README.md +2 -0
  7. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +18 -3
  8. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +330 -57
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +96 -15
  10. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +9 -6
  11. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +1 -1
  12. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
  13. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  14. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  15. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +10 -2
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  17. package/dist/src/commands/logs.js +6 -1
  18. package/dist/src/commands/run-display.d.ts +20 -0
  19. package/dist/src/commands/run-display.js +80 -1
  20. package/dist/src/commands/stats.js +47 -0
  21. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  22. package/dist/src/lib/assess-collision-detect.js +68 -4
  23. package/dist/src/lib/cli-ui/run-renderer.js +17 -9
  24. package/dist/src/lib/errors.d.ts +6 -0
  25. package/dist/src/lib/errors.js +9 -2
  26. package/dist/src/lib/manifest.js +1 -17
  27. package/dist/src/lib/version-check.js +1 -5
  28. package/dist/src/lib/workflow/batch-executor.d.ts +13 -0
  29. package/dist/src/lib/workflow/batch-executor.js +81 -18
  30. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  31. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  32. package/dist/src/lib/workflow/chain-resume.d.ts +116 -0
  33. package/dist/src/lib/workflow/chain-resume.js +166 -0
  34. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  35. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  36. package/dist/src/lib/workflow/drivers/claude-code.d.ts +7 -0
  37. package/dist/src/lib/workflow/drivers/claude-code.js +30 -6
  38. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  39. package/dist/src/lib/workflow/error-classifier.js +14 -1
  40. package/dist/src/lib/workflow/log-writer.js +6 -8
  41. package/dist/src/lib/workflow/metrics-schema.d.ts +39 -0
  42. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  43. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  44. package/dist/src/lib/workflow/phase-executor.d.ts +32 -0
  45. package/dist/src/lib/workflow/phase-executor.js +77 -5
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +23 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +45 -1
  48. package/dist/src/lib/workflow/run-orchestrator.d.ts +14 -0
  49. package/dist/src/lib/workflow/run-orchestrator.js +291 -30
  50. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  51. package/dist/src/lib/workflow/status-derivation.js +27 -0
  52. package/dist/src/lib/workflow/types.d.ts +23 -0
  53. package/dist/src/lib/workflow/worktree-manager.d.ts +43 -1
  54. package/dist/src/lib/workflow/worktree-manager.js +103 -33
  55. package/dist/src/mcp/tools/run.d.ts +2 -0
  56. package/dist/src/mcp/tools/run.js +2 -0
  57. package/package.json +2 -4
  58. package/templates/hooks/post-tool.sh +18 -3
  59. package/templates/hooks/pre-tool.sh +330 -57
  60. package/templates/scripts/cleanup-worktree.sh +103 -14
  61. package/templates/skills/assess/SKILL.md +96 -15
  62. package/templates/skills/assess/references/predicted-collision-detection.md +9 -6
  63. package/templates/skills/fullsolve/SKILL.md +1 -1
  64. package/templates/skills/reflect/SKILL.md +27 -13
  65. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  66. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  67. package/templates/skills/release/SKILL.md +10 -2
  68. package/templates/skills/spec/references/verification-criteria.md +1 -1
@@ -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, BillingError } 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,
@@ -746,6 +787,32 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
746
787
  if (lastResult.capped) {
747
788
  return lastResult;
748
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
+ }
749
816
  // Genuine failure (took long enough to be real work) → skip cold-start retries.
750
817
  // Use error classification (AC-9): if the error is retryable (e.g., API
751
818
  // rate limit, transient 503), allow one more attempt even for genuine failures.
@@ -793,11 +860,16 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
793
860
  // intent is documented and future code paths can't accidentally re-spawn a
794
861
  // capped phase without MCP.
795
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;
796
867
  if (config.mcp &&
797
868
  !lastResult.success &&
798
869
  !skipColdStartRetry &&
799
870
  !failureIsBilling &&
800
- !failureIsCapped) {
871
+ !failureIsCapped &&
872
+ !failureIsRateLimited) {
801
873
  bracketedConsoleLog(spinner, chalk.yellow(`\n ! Phase failed with MCP enabled, retrying without MCP...`));
802
874
  // Create config copy with MCP disabled
803
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>>;
@@ -170,6 +172,8 @@ export declare const PhaseLogSchema: z.ZodObject<{
170
172
  api_error: "api_error";
171
173
  hook_failure: "hook_failure";
172
174
  build_error: "build_error";
175
+ rate_limit: "rate_limit";
176
+ billing: "billing";
173
177
  }>;
174
178
  errorType: z.ZodOptional<z.ZodString>;
175
179
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -247,6 +251,8 @@ export declare const IssueLogSchema: z.ZodObject<{
247
251
  api_error: "api_error";
248
252
  hook_failure: "hook_failure";
249
253
  build_error: "build_error";
254
+ rate_limit: "rate_limit";
255
+ billing: "billing";
250
256
  }>;
251
257
  errorType: z.ZodOptional<z.ZodString>;
252
258
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -277,6 +283,7 @@ export declare const RunSummarySchema: z.ZodObject<{
277
283
  totalIssues: z.ZodNumber;
278
284
  passed: z.ZodNumber;
279
285
  failed: z.ZodNumber;
286
+ partial: z.ZodDefault<z.ZodNumber>;
280
287
  totalDurationSeconds: z.ZodNumber;
281
288
  }, z.core.$strip>;
282
289
  export type RunSummary = z.infer<typeof RunSummarySchema>;
@@ -365,6 +372,8 @@ export declare const RunLogSchema: z.ZodObject<{
365
372
  api_error: "api_error";
366
373
  hook_failure: "hook_failure";
367
374
  build_error: "build_error";
375
+ rate_limit: "rate_limit";
376
+ billing: "billing";
368
377
  }>;
369
378
  errorType: z.ZodOptional<z.ZodString>;
370
379
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -379,6 +388,7 @@ export declare const RunLogSchema: z.ZodObject<{
379
388
  totalIssues: z.ZodNumber;
380
389
  passed: z.ZodNumber;
381
390
  failed: z.ZodNumber;
391
+ partial: z.ZodDefault<z.ZodNumber>;
382
392
  totalDurationSeconds: z.ZodNumber;
383
393
  }, z.core.$strip>;
384
394
  startCommit: z.ZodOptional<z.ZodString>;
@@ -439,3 +449,16 @@ export declare function completePhaseLog(phaseLog: Omit<PhaseLog, "endTime" | "d
439
449
  export declare function finalizeRunLog(runLog: Omit<RunLog, "endTime">, options?: {
440
450
  endCommit?: string;
441
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" */
@@ -206,6 +212,13 @@ export const RunSummarySchema = z.object({
206
212
  passed: z.number().int().nonnegative(),
207
213
  /** Number of issues that failed */
208
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),
209
222
  /** Total execution time in seconds */
210
223
  totalDurationSeconds: z.number().nonnegative(),
211
224
  });
@@ -274,6 +287,7 @@ export function createEmptyRunLog(config, options) {
274
287
  totalIssues: 0,
275
288
  passed: 0,
276
289
  failed: 0,
290
+ partial: 0,
277
291
  totalDurationSeconds: 0,
278
292
  },
279
293
  startCommit: options?.startCommit,
@@ -326,6 +340,9 @@ export function finalizeRunLog(runLog, options) {
326
340
  const totalDurationSeconds = (endTime.getTime() - startTime.getTime()) / 1000;
327
341
  const passed = runLog.issues.filter((i) => i.status === "success").length;
328
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;
329
346
  return {
330
347
  ...runLog,
331
348
  endTime: endTime.toISOString(),
@@ -333,8 +350,35 @@ export function finalizeRunLog(runLog, options) {
333
350
  totalIssues: runLog.issues.length,
334
351
  passed,
335
352
  failed,
353
+ partial,
336
354
  totalDurationSeconds,
337
355
  },
338
356
  endCommit: options?.endCommit ?? runLog.endCommit,
339
357
  };
340
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") */