sequant 2.9.0 → 2.11.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 +20 -5
- package/dist/bin/cli.js +143 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +31 -3
- package/dist/src/commands/ready.js +53 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +38 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +87 -0
- package/dist/src/lib/settings.js +106 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +552 -60
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -27,6 +27,8 @@ function formatSignalLine(issue, pid, result) {
|
|
|
27
27
|
return ` Refused to signal PID ${pid} for #${issue} (matches this process or its parent)`;
|
|
28
28
|
case "pid-dead":
|
|
29
29
|
return ` Could not signal PID ${pid} for #${issue} (already exited)`;
|
|
30
|
+
case "stale-pid-untrusted":
|
|
31
|
+
return ` Refused to signal PID ${pid} for #${issue} (lock is past the age ceiling; that PID has likely been recycled)`;
|
|
30
32
|
case "kill-failed":
|
|
31
33
|
return ` Could not signal PID ${pid} for #${issue} (kill syscall failed)`;
|
|
32
34
|
case "orchestrator":
|
|
@@ -69,11 +71,13 @@ function isWorktreeDirty(worktreePath) {
|
|
|
69
71
|
}
|
|
70
72
|
return result.stdout.trim().length > 0;
|
|
71
73
|
}
|
|
72
|
-
import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, emitRunIdLine, } from "./batch-executor.js";
|
|
74
|
+
import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, recordIssueCompletion, emitRunIdLine, } from "./batch-executor.js";
|
|
73
75
|
import { reconcileStateAtStartup } from "./state-utils.js";
|
|
74
76
|
import { runChainPreflight } from "./chain-preflight.js";
|
|
77
|
+
import { runSkillsPreflight } from "./skills-preflight.js";
|
|
75
78
|
import { getCommitHash } from "./git-diff-utils.js";
|
|
76
79
|
import { planChainResumeFromState, } from "./chain-resume.js";
|
|
80
|
+
import { isCompletedIssueStatus } from "./completed-status.js";
|
|
77
81
|
import { MetricsWriter } from "./metrics-writer.js";
|
|
78
82
|
import { WorkflowEventEmitter } from "./event-emitter.js";
|
|
79
83
|
import { determineOutcome } from "./metrics-schema.js";
|
|
@@ -131,6 +135,20 @@ export class RunOrchestrator {
|
|
|
131
135
|
getEmitter() {
|
|
132
136
|
return this.emitter;
|
|
133
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The wrapped progress sink handed to the batch executor — every phase
|
|
140
|
+
* lifecycle event enters the orchestrator through here.
|
|
141
|
+
*
|
|
142
|
+
* @internal Exported for testing only. `applyProgressEvent` is private and
|
|
143
|
+
* depends on `issueStates`/`phaseStartTimes`/`emitter`, so there is no way
|
|
144
|
+
* to exercise its branches short of a full `run()`. Without this seam the
|
|
145
|
+
* #804 `waiting` branch would be untestable — and an untested `waiting`
|
|
146
|
+
* branch is not cosmetic: falling through to the complete/failed handler
|
|
147
|
+
* marks a merely-paused phase `failed` and emits `phase_failed`.
|
|
148
|
+
*/
|
|
149
|
+
getProgressCallback() {
|
|
150
|
+
return this.cfg.onProgress;
|
|
151
|
+
}
|
|
134
152
|
/**
|
|
135
153
|
* Point-in-time view of the entire run.
|
|
136
154
|
*
|
|
@@ -197,6 +215,16 @@ export class RunOrchestrator {
|
|
|
197
215
|
const wasStatus = state.status;
|
|
198
216
|
if (!state.startedAt)
|
|
199
217
|
state.startedAt = new Date();
|
|
218
|
+
// #866: a phase starting means the issue is live again, so any
|
|
219
|
+
// `completedAt` from an earlier terminal verdict is stale and must not
|
|
220
|
+
// outlive it. The #766 quality-loop recovery path reaches here holding
|
|
221
|
+
// one: a non-loop failure pins `failed` and stamps `completedAt` below,
|
|
222
|
+
// then iteration 2 re-runs the phase through this branch. Leaving it set
|
|
223
|
+
// published a completion time for a running issue — which froze the TUI's
|
|
224
|
+
// header clock for the whole recovery window. `startedAt` is deliberately
|
|
225
|
+
// NOT reset: the header measures the issue's total wall clock, spanning
|
|
226
|
+
// every iteration.
|
|
227
|
+
state.completedAt = undefined;
|
|
200
228
|
state.status = "running";
|
|
201
229
|
const now = new Date();
|
|
202
230
|
this.phaseStartTimes.set(`${issue}:${phase}`, now.getTime());
|
|
@@ -224,6 +252,25 @@ export class RunOrchestrator {
|
|
|
224
252
|
}
|
|
225
253
|
return;
|
|
226
254
|
}
|
|
255
|
+
// #804 AC-7: an auto-wait is a *paused running* phase, not a terminal one.
|
|
256
|
+
// This branch must precede the complete/failed fall-through below, which
|
|
257
|
+
// would otherwise close out the phase the moment a wait began. Surfaces the
|
|
258
|
+
// wait through the same `nowLine` the TUI already renders.
|
|
259
|
+
if (event === "waiting") {
|
|
260
|
+
if (!state.currentPhase || state.currentPhase.name !== phase)
|
|
261
|
+
return;
|
|
262
|
+
const line = extra?.text;
|
|
263
|
+
if (!line)
|
|
264
|
+
return;
|
|
265
|
+
state.currentPhase.nowLine = line;
|
|
266
|
+
state.currentPhase.lastActivityAt = new Date();
|
|
267
|
+
void this.emitter.emit("progress", {
|
|
268
|
+
issueNumber: issue,
|
|
269
|
+
phase,
|
|
270
|
+
text: line,
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
227
274
|
if (event === "activity") {
|
|
228
275
|
// Ignore activity for stale phases (race between completion and a
|
|
229
276
|
// final flushed output chunk).
|
|
@@ -369,6 +416,15 @@ export class RunOrchestrator {
|
|
|
369
416
|
*/
|
|
370
417
|
static async run(init, issueArgs, batches) {
|
|
371
418
|
const { manifest, onProgress, phasePauseHandle, settings } = init;
|
|
419
|
+
// #867: capture the run's start before any service/log setup, so wall clock
|
|
420
|
+
// is independent of whether the log writer exists (AC-3: correct under
|
|
421
|
+
// --no-log and when log init fails). This single origin is the source of
|
|
422
|
+
// truth for both the printed summary and the run log (AC-2).
|
|
423
|
+
const runStartedAt = Date.now();
|
|
424
|
+
// Every `return` below — the success path and all six early exits — closes
|
|
425
|
+
// the bracket the same way, so the subtraction lives in one place rather
|
|
426
|
+
// than being restated per exit and left to drift.
|
|
427
|
+
const wallClock = () => (Date.now() - runStartedAt) / 1000;
|
|
372
428
|
// ── Config resolution ──────────────────────────────────────────────
|
|
373
429
|
const resolved = RunOrchestrator.resolveConfig(init, issueArgs, batches);
|
|
374
430
|
const { mergedOptions, config, baseBranch } = resolved;
|
|
@@ -384,6 +440,7 @@ export class RunOrchestrator {
|
|
|
384
440
|
config,
|
|
385
441
|
mergedOptions,
|
|
386
442
|
logWriter: null,
|
|
443
|
+
wallClockDurationSeconds: wallClock(),
|
|
387
444
|
};
|
|
388
445
|
}
|
|
389
446
|
// ── Services setup ─────────────────────────────────────────────────
|
|
@@ -405,6 +462,10 @@ export class RunOrchestrator {
|
|
|
405
462
|
logPath: mergedOptions.logPath ?? settings.run.logPath,
|
|
406
463
|
verbose: config.verbose,
|
|
407
464
|
startCommit: getCommitHash(process.cwd()),
|
|
465
|
+
// #867: share the orchestrator's run origin so the log's stored
|
|
466
|
+
// wall clock (finalizeRunLog: (end-start)/1000) is derived from the
|
|
467
|
+
// same start as the summary — AC-2/AC-7 (one producer, two consumers).
|
|
468
|
+
startTime: new Date(runStartedAt),
|
|
408
469
|
});
|
|
409
470
|
await logWriter.initialize(runConfig);
|
|
410
471
|
const runId = logWriter.getRunId();
|
|
@@ -424,8 +485,12 @@ export class RunOrchestrator {
|
|
|
424
485
|
const shutdown = new ShutdownManager();
|
|
425
486
|
if (logWriter) {
|
|
426
487
|
const writer = logWriter;
|
|
427
|
-
|
|
428
|
-
|
|
488
|
+
// #856: forward the abort cause into the log. This cleanup runs on the
|
|
489
|
+
// SIGINT/SIGTERM path, where no phase result will ever arrive for the
|
|
490
|
+
// in-flight issue — without the context, `finalize()` writes that issue
|
|
491
|
+
// out as if the run had simply ended.
|
|
492
|
+
shutdown.registerCleanup("Finalize run logs", async (abort) => {
|
|
493
|
+
await writer.finalize(abort ? { aborted: abort } : undefined);
|
|
429
494
|
});
|
|
430
495
|
}
|
|
431
496
|
// ── Pre-flight state guard ─────────────────────────────────────────
|
|
@@ -478,6 +543,7 @@ export class RunOrchestrator {
|
|
|
478
543
|
config,
|
|
479
544
|
mergedOptions,
|
|
480
545
|
logWriter: null,
|
|
546
|
+
wallClockDurationSeconds: wallClock(),
|
|
481
547
|
};
|
|
482
548
|
}
|
|
483
549
|
if (plan.allComplete) {
|
|
@@ -492,6 +558,7 @@ export class RunOrchestrator {
|
|
|
492
558
|
config,
|
|
493
559
|
mergedOptions,
|
|
494
560
|
logWriter: null,
|
|
561
|
+
wallClockDurationSeconds: wallClock(),
|
|
495
562
|
};
|
|
496
563
|
}
|
|
497
564
|
if (plan.skipped.length > 0) {
|
|
@@ -513,9 +580,7 @@ export class RunOrchestrator {
|
|
|
513
580
|
for (const issueNumber of issueNumbers) {
|
|
514
581
|
try {
|
|
515
582
|
const issueState = await stateManager.getIssueState(issueNumber);
|
|
516
|
-
if (issueState &&
|
|
517
|
-
(issueState.status === "ready_for_merge" ||
|
|
518
|
-
issueState.status === "merged")) {
|
|
583
|
+
if (issueState && isCompletedIssueStatus(issueState.status)) {
|
|
519
584
|
console.log(chalk.yellow(` ! #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
|
|
520
585
|
}
|
|
521
586
|
else {
|
|
@@ -541,6 +606,7 @@ export class RunOrchestrator {
|
|
|
541
606
|
config,
|
|
542
607
|
mergedOptions,
|
|
543
608
|
logWriter: null,
|
|
609
|
+
wallClockDurationSeconds: wallClock(),
|
|
544
610
|
};
|
|
545
611
|
}
|
|
546
612
|
}
|
|
@@ -600,6 +666,7 @@ export class RunOrchestrator {
|
|
|
600
666
|
config,
|
|
601
667
|
mergedOptions,
|
|
602
668
|
logWriter: null,
|
|
669
|
+
wallClockDurationSeconds: wallClock(),
|
|
603
670
|
};
|
|
604
671
|
}
|
|
605
672
|
}
|
|
@@ -630,6 +697,7 @@ export class RunOrchestrator {
|
|
|
630
697
|
config,
|
|
631
698
|
mergedOptions,
|
|
632
699
|
logWriter: null,
|
|
700
|
+
wallClockDurationSeconds: wallClock(),
|
|
633
701
|
};
|
|
634
702
|
}
|
|
635
703
|
}
|
|
@@ -639,6 +707,49 @@ export class RunOrchestrator {
|
|
|
639
707
|
for (const issueNumber of issueNumbers) {
|
|
640
708
|
issueInfoMap.set(issueNumber, await getIssueInfo(issueNumber));
|
|
641
709
|
}
|
|
710
|
+
// ── Skills pre-flight (#813) ───────────────────────────────────────
|
|
711
|
+
// Fail fast when the driver resolves phases via `.claude/skills/` and a
|
|
712
|
+
// required skill is missing — BEFORE any worktree is provisioned or
|
|
713
|
+
// per-issue state entry written. Without this, the phase agent hunts for
|
|
714
|
+
// a slash command that can never resolve and the run surfaces as a bogus
|
|
715
|
+
// "spec retry" failure. Skipped for non-skill drivers (aider) and for
|
|
716
|
+
// --dry-run (no agent executes).
|
|
717
|
+
if (!config.dryRun) {
|
|
718
|
+
const preflight = await runSkillsPreflight({
|
|
719
|
+
agent: config.agent,
|
|
720
|
+
aiderSettings: config.aiderSettings,
|
|
721
|
+
phases: config.phases,
|
|
722
|
+
autoDetectPhases: resolved.autoDetectPhases,
|
|
723
|
+
qualityLoop: config.qualityLoop,
|
|
724
|
+
testgen: mergedOptions.testgen,
|
|
725
|
+
securityReview: mergedOptions.securityReview,
|
|
726
|
+
issueNumbers,
|
|
727
|
+
issueInfoMap,
|
|
728
|
+
});
|
|
729
|
+
if (!preflight.ok) {
|
|
730
|
+
bracketedConsoleLog(phasePauseHandle, chalk.red(`\n ✖ Skills pre-flight failed: ${preflight.cause}`));
|
|
731
|
+
bracketedConsoleLog(phasePauseHandle, chalk.red(` ${preflight.remedy}`));
|
|
732
|
+
shutdown.dispose();
|
|
733
|
+
return {
|
|
734
|
+
results: issueNumbers.map((issueNumber) => ({
|
|
735
|
+
issueNumber,
|
|
736
|
+
success: false,
|
|
737
|
+
phaseResults: [],
|
|
738
|
+
durationSeconds: 0,
|
|
739
|
+
loopTriggered: false,
|
|
740
|
+
abortReason: `skills pre-flight failed: ${preflight.cause}`,
|
|
741
|
+
})),
|
|
742
|
+
logPath: null,
|
|
743
|
+
exitCode: 1,
|
|
744
|
+
worktreeMap: new Map(),
|
|
745
|
+
issueInfoMap,
|
|
746
|
+
config,
|
|
747
|
+
mergedOptions,
|
|
748
|
+
logWriter: null,
|
|
749
|
+
wallClockDurationSeconds: wallClock(),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
642
753
|
const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
|
|
643
754
|
let worktreeMap = new Map();
|
|
644
755
|
if (useWorktreeIsolation && !config.dryRun) {
|
|
@@ -710,10 +821,15 @@ export class RunOrchestrator {
|
|
|
710
821
|
endCommit: getCommitHash(process.cwd()),
|
|
711
822
|
});
|
|
712
823
|
}
|
|
824
|
+
// #867: the run's wall clock, computed once from the single origin. Both
|
|
825
|
+
// the metrics record and the RunResult below derive from this value — no
|
|
826
|
+
// consumer re-sums per-issue durations (which double-counts under
|
|
827
|
+
// --parallel). Same notion the log stores via finalizeRunLog above.
|
|
828
|
+
const wallClockDurationSeconds = wallClock();
|
|
713
829
|
// ── Record metrics ─────────────────────────────────────────────
|
|
714
830
|
if (!config.dryRun && results.length > 0) {
|
|
715
831
|
try {
|
|
716
|
-
await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers);
|
|
832
|
+
await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers, wallClockDurationSeconds);
|
|
717
833
|
}
|
|
718
834
|
catch (metricsError) {
|
|
719
835
|
logNonFatalWarning(" ! Metrics recording failed, continuing...", metricsError, config.verbose);
|
|
@@ -729,6 +845,7 @@ export class RunOrchestrator {
|
|
|
729
845
|
config,
|
|
730
846
|
mergedOptions,
|
|
731
847
|
logWriter,
|
|
848
|
+
wallClockDurationSeconds,
|
|
732
849
|
};
|
|
733
850
|
}
|
|
734
851
|
finally {
|
|
@@ -925,14 +1042,13 @@ export class RunOrchestrator {
|
|
|
925
1042
|
: undefined,
|
|
926
1043
|
});
|
|
927
1044
|
results.push(result);
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1045
|
+
// A chain halts on ANY failed link, QA or otherwise: every successor
|
|
1046
|
+
// rebases onto its predecessor's committed work, so continuing past a
|
|
1047
|
+
// failure would build on a broken or absent base. #795 removed a
|
|
1048
|
+
// `--qa-gate` branch here that re-tested for a QA failure before this
|
|
1049
|
+
// same unconditional break — it could never change the outcome.
|
|
1050
|
+
if (!result.success)
|
|
934
1051
|
break;
|
|
935
|
-
}
|
|
936
1052
|
}
|
|
937
1053
|
return results;
|
|
938
1054
|
}
|
|
@@ -1017,11 +1133,12 @@ export class RunOrchestrator {
|
|
|
1017
1133
|
});
|
|
1018
1134
|
}
|
|
1019
1135
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1136
|
+
// Record PR info, flip status on a PR-creation failure (#879), and
|
|
1137
|
+
// finalize — via the shared helper so this live path stays in lockstep
|
|
1138
|
+
// with executeBatch. Before #879 this block omitted the failure flip, so
|
|
1139
|
+
// a real `sequant run` left the run-log status at `success` on PR failure.
|
|
1023
1140
|
if (logWriter) {
|
|
1024
|
-
logWriter
|
|
1141
|
+
recordIssueCompletion(logWriter, result, parallelIssueNumber);
|
|
1025
1142
|
}
|
|
1026
1143
|
return result;
|
|
1027
1144
|
}
|
|
@@ -1034,9 +1151,13 @@ export class RunOrchestrator {
|
|
|
1034
1151
|
});
|
|
1035
1152
|
}
|
|
1036
1153
|
}
|
|
1037
|
-
static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers) {
|
|
1154
|
+
static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers, wallClockDurationSeconds) {
|
|
1038
1155
|
const metricsWriter = new MetricsWriter({ verbose: config.verbose });
|
|
1039
|
-
|
|
1156
|
+
// #867: run wall clock, not the sum of per-issue durations — under
|
|
1157
|
+
// --parallel that sum double-counts overlapping issues (same defect the
|
|
1158
|
+
// SUMMARY header had). The orchestrator owns the run's start/end and passes
|
|
1159
|
+
// the single authoritative value in.
|
|
1160
|
+
const totalDuration = wallClockDurationSeconds;
|
|
1040
1161
|
const allPhases = new Set();
|
|
1041
1162
|
for (const result of results) {
|
|
1042
1163
|
for (const pr of result.phaseResults) {
|
|
@@ -1086,6 +1207,29 @@ export class RunOrchestrator {
|
|
|
1086
1207
|
// issue is the halting one; in parallel runs it is the earliest-listed
|
|
1087
1208
|
// failure. Undefined on success — the field is omitted from the record.
|
|
1088
1209
|
const failureCategory = results.find((r) => !r.success && r.failureCategory)?.failureCategory;
|
|
1210
|
+
// #915: one entry per escalated phase execution, from both retry sites —
|
|
1211
|
+
// the outer quality loop (`phaseResults[].escalatedEffort`) and the
|
|
1212
|
+
// `--ready-gate` QA-pass loop (`readyGate.effortEscalations`).
|
|
1213
|
+
const effortEscalations = [
|
|
1214
|
+
...results.flatMap((r) => r.phaseResults
|
|
1215
|
+
.filter((p) => p.escalatedEffort)
|
|
1216
|
+
.map((p) => ({
|
|
1217
|
+
phase: p.phase,
|
|
1218
|
+
base: p.escalatedEffort.base,
|
|
1219
|
+
escalated: p.escalatedEffort.escalated,
|
|
1220
|
+
}))),
|
|
1221
|
+
...results.flatMap((r) => r.readyGate?.effortEscalations ?? []),
|
|
1222
|
+
];
|
|
1223
|
+
// #915: the outer quality loop already logs its own escalations live at
|
|
1224
|
+
// the batch-executor dispatch site — only the `--ready-gate` QA-pass loop
|
|
1225
|
+
// has no equivalent live print, so surface those here.
|
|
1226
|
+
if (config.verbose) {
|
|
1227
|
+
for (const r of results) {
|
|
1228
|
+
for (const e of r.readyGate?.effortEscalations ?? []) {
|
|
1229
|
+
console.log(chalk.gray(` effort: ${e.base} → ${e.escalated} (ready-gate retry, #${r.issueNumber})`));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1089
1233
|
await metricsWriter.recordRun({
|
|
1090
1234
|
issues: issueNumbers,
|
|
1091
1235
|
phases: Array.from(allPhases),
|
|
@@ -1094,6 +1238,10 @@ export class RunOrchestrator {
|
|
|
1094
1238
|
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
1095
1239
|
flags: cliFlags,
|
|
1096
1240
|
failureCategory,
|
|
1241
|
+
// #914: resolved per-phase model/effort, when any phase had one.
|
|
1242
|
+
phasePolicies: config.phasePolicies,
|
|
1243
|
+
// #915: escalated tiers, when any phase execution escalated.
|
|
1244
|
+
effortEscalations,
|
|
1097
1245
|
metrics: {
|
|
1098
1246
|
tokensUsed: tokenUsage.tokensUsed,
|
|
1099
1247
|
filesChanged: totalFilesChanged,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills pre-flight for `sequant run` (#813).
|
|
3
|
+
*
|
|
4
|
+
* The claude-code driver executes phases as slash-command skills loaded from
|
|
5
|
+
* project scope only (`settingSources: ["project"]`, #19/#711). Without
|
|
6
|
+
* `.claude/skills/` the phase agent hunts for a command that can never
|
|
7
|
+
* resolve, does no work, and the run surfaces as a bogus "spec retry"
|
|
8
|
+
* failure. This module computes the skills a run actually needs — from the
|
|
9
|
+
* phases resolved for that run, not a hardcoded triple — and checks them via
|
|
10
|
+
* the same `checkSkillsInstalled` helper `doctor` uses, so the two cannot
|
|
11
|
+
* drift.
|
|
12
|
+
*
|
|
13
|
+
* Drivers whose phase prompts do the work inline (aider's `driverOverrides`
|
|
14
|
+
* templates in phase-registry.ts) never resolve skills, so the pre-flight is
|
|
15
|
+
* skipped for them via `AgentDriver.resolvesSkills`.
|
|
16
|
+
*/
|
|
17
|
+
import type { AiderSettings } from "../settings.js";
|
|
18
|
+
import type { Phase } from "./types.js";
|
|
19
|
+
export interface SkillsPreflightInput {
|
|
20
|
+
/** Agent driver name (default claude-code). */
|
|
21
|
+
agent?: string;
|
|
22
|
+
/** Aider settings, forwarded to the driver factory. */
|
|
23
|
+
aiderSettings?: AiderSettings;
|
|
24
|
+
/** Base pipeline for explicit-phase runs (`config.phases`). */
|
|
25
|
+
phases: Phase[];
|
|
26
|
+
/** True when phases are auto-detected from labels (no explicit --phases). */
|
|
27
|
+
autoDetectPhases: boolean;
|
|
28
|
+
/** True when the quality loop may invoke the loop skill. */
|
|
29
|
+
qualityLoop: boolean;
|
|
30
|
+
/** Additive phase flags (`--testgen` / `--security-review`). */
|
|
31
|
+
testgen?: boolean;
|
|
32
|
+
securityReview?: boolean;
|
|
33
|
+
/** Issues in the run, with their labels (drives per-issue phase rules). */
|
|
34
|
+
issueNumbers: number[];
|
|
35
|
+
issueInfoMap: Map<number, {
|
|
36
|
+
title: string;
|
|
37
|
+
labels: string[];
|
|
38
|
+
}>;
|
|
39
|
+
/** Project root to check under (default: `process.cwd()`). */
|
|
40
|
+
cwd?: string;
|
|
41
|
+
}
|
|
42
|
+
export type SkillsPreflightResult = {
|
|
43
|
+
ok: true;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
/** Human-readable cause, e.g. `missing skills: spec, exec, qa`. */
|
|
47
|
+
cause: string;
|
|
48
|
+
/** Missing skill names, in required order. */
|
|
49
|
+
missingSkills: string[];
|
|
50
|
+
/** Driver whose skill resolution triggered the check. */
|
|
51
|
+
driverName: string;
|
|
52
|
+
/** Remedy line for display. */
|
|
53
|
+
remedy: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Compute the union of skills required by the phases resolved for this run.
|
|
57
|
+
*
|
|
58
|
+
* Explicit-phase runs start from `phases` as given; auto-detect runs start
|
|
59
|
+
* from each issue's label-detected pipeline. Both then apply the additive
|
|
60
|
+
* `--testgen` / `--security-review` / UI-label rules via
|
|
61
|
+
* `determinePhasesForIssue`.
|
|
62
|
+
*
|
|
63
|
+
* Two deliberate over-approximations keep late-added phases covered:
|
|
64
|
+
*
|
|
65
|
+
* - An explicit `--testgen` / `--security-review` flag requires its skill
|
|
66
|
+
* unconditionally, even when `determinePhasesForIssue` would not insert
|
|
67
|
+
* the phase because `spec` is absent from the pipeline. On a resume where
|
|
68
|
+
* spec already completed, batch-executor inserts the phase anyway
|
|
69
|
+
* (`phases.includes("spec") || specAlreadyRan`), and the pre-flight cannot
|
|
70
|
+
* cheaply know `specAlreadyRan` — requiring the skill the user asked for
|
|
71
|
+
* is the safe superset.
|
|
72
|
+
* - The loop skill is required when the quality loop is enabled up front OR
|
|
73
|
+
* when any issue's labels would auto-enable it (`complex`/`refactor`/...,
|
|
74
|
+
* via `detectPhasesFromLabels().qualityLoop`), since the loop skill is
|
|
75
|
+
* invoked the same way as any phase skill.
|
|
76
|
+
*
|
|
77
|
+
* Phases recommended later by spec output (`parseRecommendedWorkflow`)
|
|
78
|
+
* remain unknowable at pre-flight time — the accepted gap documented on
|
|
79
|
+
* #813.
|
|
80
|
+
*
|
|
81
|
+
* Exported for direct unit testing (AC-2).
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveRequiredSkills(input: Pick<SkillsPreflightInput, "phases" | "autoDetectPhases" | "qualityLoop" | "testgen" | "securityReview" | "issueNumbers" | "issueInfoMap">): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Run the skills pre-flight. Returns `{ok: true}` when the run may proceed:
|
|
86
|
+
* either every required skill is installed, or the selected driver does not
|
|
87
|
+
* resolve skills at all (AC-3).
|
|
88
|
+
*/
|
|
89
|
+
export declare function runSkillsPreflight(input: SkillsPreflightInput): Promise<SkillsPreflightResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills pre-flight for `sequant run` (#813).
|
|
3
|
+
*
|
|
4
|
+
* The claude-code driver executes phases as slash-command skills loaded from
|
|
5
|
+
* project scope only (`settingSources: ["project"]`, #19/#711). Without
|
|
6
|
+
* `.claude/skills/` the phase agent hunts for a command that can never
|
|
7
|
+
* resolve, does no work, and the run surfaces as a bogus "spec retry"
|
|
8
|
+
* failure. This module computes the skills a run actually needs — from the
|
|
9
|
+
* phases resolved for that run, not a hardcoded triple — and checks them via
|
|
10
|
+
* the same `checkSkillsInstalled` helper `doctor` uses, so the two cannot
|
|
11
|
+
* drift.
|
|
12
|
+
*
|
|
13
|
+
* Drivers whose phase prompts do the work inline (aider's `driverOverrides`
|
|
14
|
+
* templates in phase-registry.ts) never resolve skills, so the pre-flight is
|
|
15
|
+
* skipped for them via `AgentDriver.resolvesSkills`.
|
|
16
|
+
*/
|
|
17
|
+
import { getDriver } from "./drivers/index.js";
|
|
18
|
+
import { detectPhasesFromLabels, determinePhasesForIssue, } from "./phase-mapper.js";
|
|
19
|
+
import { phaseRegistry } from "./phase-registry.js";
|
|
20
|
+
import { checkSkillsInstalled, SKILLS_DIR } from "../skills-check.js";
|
|
21
|
+
/**
|
|
22
|
+
* Compute the union of skills required by the phases resolved for this run.
|
|
23
|
+
*
|
|
24
|
+
* Explicit-phase runs start from `phases` as given; auto-detect runs start
|
|
25
|
+
* from each issue's label-detected pipeline. Both then apply the additive
|
|
26
|
+
* `--testgen` / `--security-review` / UI-label rules via
|
|
27
|
+
* `determinePhasesForIssue`.
|
|
28
|
+
*
|
|
29
|
+
* Two deliberate over-approximations keep late-added phases covered:
|
|
30
|
+
*
|
|
31
|
+
* - An explicit `--testgen` / `--security-review` flag requires its skill
|
|
32
|
+
* unconditionally, even when `determinePhasesForIssue` would not insert
|
|
33
|
+
* the phase because `spec` is absent from the pipeline. On a resume where
|
|
34
|
+
* spec already completed, batch-executor inserts the phase anyway
|
|
35
|
+
* (`phases.includes("spec") || specAlreadyRan`), and the pre-flight cannot
|
|
36
|
+
* cheaply know `specAlreadyRan` — requiring the skill the user asked for
|
|
37
|
+
* is the safe superset.
|
|
38
|
+
* - The loop skill is required when the quality loop is enabled up front OR
|
|
39
|
+
* when any issue's labels would auto-enable it (`complex`/`refactor`/...,
|
|
40
|
+
* via `detectPhasesFromLabels().qualityLoop`), since the loop skill is
|
|
41
|
+
* invoked the same way as any phase skill.
|
|
42
|
+
*
|
|
43
|
+
* Phases recommended later by spec output (`parseRecommendedWorkflow`)
|
|
44
|
+
* remain unknowable at pre-flight time — the accepted gap documented on
|
|
45
|
+
* #813.
|
|
46
|
+
*
|
|
47
|
+
* Exported for direct unit testing (AC-2).
|
|
48
|
+
*/
|
|
49
|
+
export function resolveRequiredSkills(input) {
|
|
50
|
+
const additiveFlags = {
|
|
51
|
+
testgen: input.testgen,
|
|
52
|
+
securityReview: input.securityReview,
|
|
53
|
+
};
|
|
54
|
+
const requiredPhases = new Set();
|
|
55
|
+
let qualityLoop = input.qualityLoop;
|
|
56
|
+
for (const issueNumber of input.issueNumbers) {
|
|
57
|
+
const labels = input.issueInfoMap.get(issueNumber)?.labels ?? [];
|
|
58
|
+
const detected = input.autoDetectPhases
|
|
59
|
+
? detectPhasesFromLabels(labels)
|
|
60
|
+
: null;
|
|
61
|
+
if (detected?.qualityLoop)
|
|
62
|
+
qualityLoop = true;
|
|
63
|
+
const basePhases = detected ? detected.phases : input.phases;
|
|
64
|
+
for (const phase of determinePhasesForIssue(basePhases, labels, additiveFlags)) {
|
|
65
|
+
requiredPhases.add(phase);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (input.testgen)
|
|
69
|
+
requiredPhases.add("testgen");
|
|
70
|
+
if (input.securityReview)
|
|
71
|
+
requiredPhases.add("security-review");
|
|
72
|
+
if (qualityLoop)
|
|
73
|
+
requiredPhases.add("loop");
|
|
74
|
+
return [...requiredPhases].map((phase) => phaseRegistry.get(phase).skill);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run the skills pre-flight. Returns `{ok: true}` when the run may proceed:
|
|
78
|
+
* either every required skill is installed, or the selected driver does not
|
|
79
|
+
* resolve skills at all (AC-3).
|
|
80
|
+
*/
|
|
81
|
+
export async function runSkillsPreflight(input) {
|
|
82
|
+
let driver;
|
|
83
|
+
try {
|
|
84
|
+
driver = getDriver(input.agent, {
|
|
85
|
+
aiderSettings: input.aiderSettings,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Unknown driver name (bad `settings.run.agent`). Don't let the
|
|
90
|
+
// pre-flight be the thing that crashes the run with a raw throw —
|
|
91
|
+
// skip it and let phase-executor surface the unknown-driver error
|
|
92
|
+
// through its normal per-issue failure path.
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
if (!driver.resolvesSkills)
|
|
96
|
+
return { ok: true };
|
|
97
|
+
const requiredSkills = resolveRequiredSkills(input);
|
|
98
|
+
const { skillsDirExists, missingSkills } = await checkSkillsInstalled(requiredSkills, input.cwd);
|
|
99
|
+
if (missingSkills.length === 0)
|
|
100
|
+
return { ok: true };
|
|
101
|
+
const cause = skillsDirExists
|
|
102
|
+
? `missing skills: ${missingSkills.join(", ")}`
|
|
103
|
+
: `missing ${SKILLS_DIR}/ directory (needs: ${missingSkills.join(", ")})`;
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
cause,
|
|
107
|
+
missingSkills,
|
|
108
|
+
driverName: driver.name,
|
|
109
|
+
remedy: `The ${driver.name} driver resolves phases from ${SKILLS_DIR}/ — ` +
|
|
110
|
+
`run \`sequant sync\` to install them, then re-run.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured resolution of the spec→run phase recommendation (#921).
|
|
3
|
+
*
|
|
4
|
+
* `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
|
|
5
|
+
* *ephemeral chat text* for a `## Recommended Workflow` section. When the
|
|
6
|
+
* spec agent posts its plan via a body file instead of restating it in
|
|
7
|
+
* chat, that regex has nothing to match and the run silently falls back to
|
|
8
|
+
* label-based phase detection — dropping any recommended phase the label
|
|
9
|
+
* fallback can never produce (e.g. `testgen`). See #814.
|
|
10
|
+
*
|
|
11
|
+
* This module resolves the recommendation through an ordered chain, each
|
|
12
|
+
* step falling through loudly to the next on failure:
|
|
13
|
+
*
|
|
14
|
+
* 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
|
|
15
|
+
* comment marker (this issue's fix)
|
|
16
|
+
* 2. `comment-prose` — the same regex as `chat`, applied to the spec
|
|
17
|
+
* plan's GitHub comment body instead of chat text
|
|
18
|
+
* 3. `chat` — `parseRecommendedWorkflow` over the agent's
|
|
19
|
+
* captured chat output (existing behavior)
|
|
20
|
+
* 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
|
|
21
|
+
*
|
|
22
|
+
* The durable comment marker is the system's existing idiom — see
|
|
23
|
+
* `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
|
|
24
|
+
* `assess-comment-parser.ts`.
|
|
25
|
+
*/
|
|
26
|
+
import type { Phase } from "./types.js";
|
|
27
|
+
import { GitHubProvider } from "./platforms/github.js";
|
|
28
|
+
import type { SpecRecommendationSource } from "./run-log-schema.js";
|
|
29
|
+
export interface ResolvedSpecRecommendation {
|
|
30
|
+
phases: Phase[];
|
|
31
|
+
qualityLoop: boolean;
|
|
32
|
+
source: SpecRecommendationSource;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract and validate the latest `SEQUANT_SPEC` marker across a set of
|
|
36
|
+
* comment bodies (oldest-to-newest order, matching `gh`'s natural order).
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` when no marker is present at all. Returns `null` and logs a
|
|
39
|
+
* visible warning when the latest marker is malformed JSON, fails schema
|
|
40
|
+
* validation, or names a phase the registry doesn't recognize — per AC-2,
|
|
41
|
+
* an invalid marker falls through to the next resolution step rather than
|
|
42
|
+
* silently dropping the recommendation or silently ignoring the bad phase.
|
|
43
|
+
*
|
|
44
|
+
* Only the *latest* marker is considered (last comment, last match within
|
|
45
|
+
* it) — a re-spec's marker supersedes an earlier one, matching how a human
|
|
46
|
+
* reader would interpret the thread.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseSpecMarker(commentBodies: string[]): {
|
|
49
|
+
phases: Phase[];
|
|
50
|
+
qualityLoop: boolean;
|
|
51
|
+
} | null;
|
|
52
|
+
export interface ResolveSpecRecommendationInput {
|
|
53
|
+
/** The spec agent's captured chat output (existing `chat` fallback). */
|
|
54
|
+
chatOutput: string;
|
|
55
|
+
/** GitHub issue number, used to fetch its comments for the marker/prose steps. */
|
|
56
|
+
issueNumber: number;
|
|
57
|
+
/** Issue labels, used for the final label-based fallback. */
|
|
58
|
+
labels: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Injectable for tests — defaults to a real `GitHubProvider`. Comment
|
|
61
|
+
* fetch failures are already handled by `fetchIssueCommentBodiesSync`
|
|
62
|
+
* (returns `[]`), so the chain degrades to `chat`/`label-fallback`
|
|
63
|
+
* gracefully when GitHub is unreachable.
|
|
64
|
+
*/
|
|
65
|
+
githubProvider?: Pick<GitHubProvider, "fetchIssueCommentBodiesSync">;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the spec→run phase recommendation through the ordered chain:
|
|
69
|
+
* comment-marker → comment-prose → chat-text → label-fallback.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveSpecRecommendation(input: ResolveSpecRecommendationInput): ResolvedSpecRecommendation;
|