sequant 2.10.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/plugin.json +1 -1
- package/README.md +6 -2
- package/dist/bin/cli.js +47 -2
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +206 -4
- package/dist/src/commands/ready.d.ts +6 -0
- package/dist/src/commands/ready.js +15 -1
- package/dist/src/commands/run-display.js +1 -0
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/cli-flags.d.ts +23 -0
- package/dist/src/lib/cli-flags.js +43 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
- package/dist/src/lib/cli-ui/run-renderer.js +7 -1
- 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 +6 -3
- package/dist/src/lib/locks/index.js +4 -2
- package/dist/src/lib/locks/lock-manager.d.ts +81 -1
- package/dist/src/lib/locks/lock-manager.js +230 -5
- package/dist/src/lib/locks/types.d.ts +72 -0
- package/dist/src/lib/locks/types.js +28 -0
- package/dist/src/lib/settings.d.ts +73 -0
- package/dist/src/lib/settings.js +45 -0
- package/dist/src/lib/test-tautology-detector.d.ts +4 -3
- package/dist/src/lib/test-tautology-detector.js +101 -41
- package/dist/src/lib/workflow/batch-executor.js +78 -19
- package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
- package/dist/src/lib/workflow/config-resolver.js +89 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
- 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/log-writer.d.ts +10 -1
- package/dist/src/lib/workflow/log-writer.js +20 -0
- package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
- 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.js +10 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
- package/dist/src/lib/workflow/ready-gate.js +24 -3
- package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
- package/dist/src/lib/workflow/run-log-schema.js +31 -1
- package/dist/src/lib/workflow/run-orchestrator.js +27 -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/types.d.ts +64 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +9 -1
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/package.json +3 -2
- package/templates/hooks/pre-tool.sh +228 -0
- package/templates/scripts/cleanup-worktree.sh +36 -15
- package/templates/scripts/new-feature.sh +25 -19
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/assess/SKILL.md +5 -1
- package/templates/skills/exec/SKILL.md +61 -7
- package/templates/skills/fullsolve/SKILL.md +127 -21
- package/templates/skills/loop/SKILL.md +56 -11
- package/templates/skills/merger/SKILL.md +98 -10
- package/templates/skills/qa/SKILL.md +59 -6
- package/templates/skills/release/SKILL.md +79 -0
- package/templates/skills/spec/SKILL.md +31 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
3
|
"description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees and quality gates, through spec → exec → qa phases.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.11.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sequant-io",
|
|
7
7
|
"email": "hello@sequant.io"
|
package/README.md
CHANGED
|
@@ -296,12 +296,16 @@ Multi-issue runs are parallel by default, and a per-issue lock (`.sequant/locks/
|
|
|
296
296
|
"run": {
|
|
297
297
|
"qualityLoop": false,
|
|
298
298
|
"maxIterations": 3,
|
|
299
|
-
"defaultBase": "feature/dashboard" // Optional: custom default base branch
|
|
299
|
+
"defaultBase": "feature/dashboard", // Optional: custom default base branch
|
|
300
|
+
"phases": {
|
|
301
|
+
"exec": { "model": "sonnet", "effort": "medium" } // Optional: per-phase model/effort override
|
|
302
|
+
},
|
|
303
|
+
"effortEscalation": false // Optional: escalate effort one tier on a quality-loop retry
|
|
300
304
|
}
|
|
301
305
|
}
|
|
302
306
|
```
|
|
303
307
|
|
|
304
|
-
See [Customization Guide](docs/guides/customization.md) for all options.
|
|
308
|
+
See [Customization Guide](docs/guides/customization.md) for all options, [Per-Phase Model & Effort](docs/reference/run-command.md#per-phase-model--effort) for the `run.phases` shape, the `--models`/`--efforts` flags, and precedence, and [Effort Escalation on Retries](docs/reference/run-command.md#effort-escalation-on-retries) for `run.effortEscalation`/`--escalate-effort`.
|
|
305
309
|
|
|
306
310
|
---
|
|
307
311
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -18,7 +18,9 @@ import { readFileSync } from "fs";
|
|
|
18
18
|
import { initCommand } from "../src/commands/init.js";
|
|
19
19
|
import { buildHomeStrayWarning, getInstallRoot, isHomeStrayInstall, isLocalNodeModulesInstall, } from "../src/lib/version-check.js";
|
|
20
20
|
import { configureUI, banner } from "../src/lib/cli-ui.js";
|
|
21
|
-
import { parseWholeNumber, parsePositiveSeconds, } from "../src/lib/cli-flags.js";
|
|
21
|
+
import { parseWholeNumber, parsePositiveSeconds, parsePhaseSpecFlag, } from "../src/lib/cli-flags.js";
|
|
22
|
+
import { getPhaseNames } from "../src/lib/workflow/phase-registry.js";
|
|
23
|
+
import { EFFORT_LEVELS } from "../src/lib/settings.js";
|
|
22
24
|
import { isCI, isStdoutTTY } from "../src/lib/tty.js";
|
|
23
25
|
import { detectPackageManagerSync, getPackageManagerCommands, } from "../src/lib/stacks.js";
|
|
24
26
|
// Read version from package.json dynamically
|
|
@@ -58,7 +60,8 @@ import { syncCommand, areSkillsOutdated, checkAndWarnSkillsOutdated, } from "../
|
|
|
58
60
|
import { mergeCommand } from "../src/commands/merge.js";
|
|
59
61
|
import { readyCommand, } from "../src/commands/ready.js";
|
|
60
62
|
import { conventionsCommand } from "../src/commands/conventions.js";
|
|
61
|
-
import { locksListCommand, locksClearCommand, locksAcquireCommand, locksReleaseCommand, locksCheckCommand, locksCheckBatchCommand, } from "../src/commands/locks.js";
|
|
63
|
+
import { locksListCommand, locksClearCommand, locksAcquireCommand, locksReleaseCommand, locksCheckCommand, locksCheckBatchCommand, locksCheckoutCommand, } from "../src/commands/locks.js";
|
|
64
|
+
import { worktreeResolveCommand, worktreeVerifyCommand, } from "../src/commands/worktree.js";
|
|
62
65
|
import { promptCommand } from "../src/commands/prompt.js";
|
|
63
66
|
import { watchCommand } from "../src/commands/watch.js";
|
|
64
67
|
import { abortCommand } from "../src/commands/abort.js";
|
|
@@ -236,6 +239,14 @@ parseWholeNumber("--auto-wait", {
|
|
|
236
239
|
// bounds (policy from settings.ready.policy, iteration cap, stagnation guard,
|
|
237
240
|
// Non-Goals) — no new policy surface. Never merges; stops at the human gate.
|
|
238
241
|
.option("--ready-gate", "After phases succeed, run the post-QA ready gate (qa→loop→qa to the configured policy) — never merges, stops at the human merge gate")
|
|
242
|
+
// #914: per-phase model/effort override for the claude-code driver. Bare
|
|
243
|
+
// value applies to every phase; comma list of phase=value pairs applies
|
|
244
|
+
// per phase. CLI > settings.run.phases > absent (resolvePhasePolicies).
|
|
245
|
+
.option("--models <spec>", "Per-phase model override, e.g. 'sonnet' or 'spec=fable,exec=sonnet' (default: none — inherits the CLI default model)", parsePhaseSpecFlag("--models", getPhaseNames()))
|
|
246
|
+
.option("--efforts <spec>", "Per-phase reasoning-effort override (low|medium|high|xhigh|max), same grammar as --models (default: none — inherits the SDK default)", parsePhaseSpecFlag("--efforts", getPhaseNames(), EFFORT_LEVELS))
|
|
247
|
+
// #915: opt-in escalation on quality-loop retries. Raises token cost, so
|
|
248
|
+
// off by default — matches --ready-gate's opt-in shape above.
|
|
249
|
+
.option("--escalate-effort", "On a quality-loop retry (loop iteration ≥ 2), run that retried phase one reasoning-effort tier above its resolved base (default: off — see --efforts)")
|
|
239
250
|
.option("-f, --force", "Force re-execution of completed issues (bypass pre-flight state guard) and take over per-issue locks")
|
|
240
251
|
.option("--signal-other", "With --force, SIGTERM the prior PID holding the lock (same-host alive only)")
|
|
241
252
|
.option("--concurrency <n>", "Max concurrent issues in parallel mode (default: 3)",
|
|
@@ -359,6 +370,11 @@ parseWholeNumber("--max-iterations", {
|
|
|
359
370
|
}))
|
|
360
371
|
.option("--timeout <seconds>", "Timeout per phase in seconds", parsePositiveSeconds("--timeout"))
|
|
361
372
|
.option("--no-mcp", "Disable MCP server injection in headless mode")
|
|
373
|
+
.option("--models <spec>", "Per-phase model override for the qa/loop phases this gate runs, e.g. 'sonnet' or 'qa=sonnet' (default: none)", parsePhaseSpecFlag("--models", getPhaseNames()))
|
|
374
|
+
.option("--efforts <spec>", "Per-phase reasoning-effort override (low|medium|high|xhigh|max), same grammar as --models (default: none)", parsePhaseSpecFlag("--efforts", getPhaseNames(), EFFORT_LEVELS))
|
|
375
|
+
// #915: same opt-in escalation as `sequant run --escalate-effort`, applied
|
|
376
|
+
// to this gate's qa/loop retry passes.
|
|
377
|
+
.option("--escalate-effort", "On a QA-pass retry (pass ≥ 2), run that retried phase one reasoning-effort tier above its resolved base (default: off)")
|
|
362
378
|
.option("--json", "Output as JSON")
|
|
363
379
|
.option("-v, --verbose", "Enable verbose output")
|
|
364
380
|
.action((issue, options) => readyCommand(issue, options));
|
|
@@ -486,6 +502,35 @@ locksCmd
|
|
|
486
502
|
.description("Batch read-only probe: emit canonical ⚠ warning lines for held issues (for /assess dashboard)")
|
|
487
503
|
.option("--json", "Output as JSON instead of canonical text lines")
|
|
488
504
|
.action(locksCheckBatchCommand);
|
|
505
|
+
// Checkout-scoped lock (#901). The per-issue locks above give no mutual
|
|
506
|
+
// exclusion on the shared working tree — two sessions on different issues take
|
|
507
|
+
// different lock files, yet `git checkout`/`reset`/`rebase`/`merge` are global
|
|
508
|
+
// to the tree. This lock represents the tree itself.
|
|
509
|
+
locksCmd
|
|
510
|
+
.command("checkout <action>")
|
|
511
|
+
.description("Working-tree lock: acquire|release|check|clear (guards branch-mutating git in the main checkout)")
|
|
512
|
+
.option("--issue <issue>", "Issue this session is working on (acquire, release; proves ownership on release)")
|
|
513
|
+
.option("--command <command>", "Human-readable command label", "unknown")
|
|
514
|
+
.option("--session-id <id>", "Claude Code session id; preferred holder identity for skill shells")
|
|
515
|
+
.option("--skip-pid-check", "Mark the lock so stale recovery skips same-host PID checks (use from skill shells)")
|
|
516
|
+
.option("-f, --force", "Clear even a fresh holder (clear)")
|
|
517
|
+
.option("--json", "Output as JSON")
|
|
518
|
+
.action(locksCheckoutCommand);
|
|
519
|
+
// Repo-scoped worktree resolution for skill bodies (#899)
|
|
520
|
+
const worktreeCmd = program
|
|
521
|
+
.command("worktree")
|
|
522
|
+
.description("Resolve and verify this repository's issue worktrees");
|
|
523
|
+
worktreeCmd
|
|
524
|
+
.command("resolve <issue>")
|
|
525
|
+
.description("Print the absolute path of this repo's worktree for an issue (exit 1 if none)")
|
|
526
|
+
.option("--json", "Output as JSON")
|
|
527
|
+
.action(worktreeResolveCommand);
|
|
528
|
+
worktreeCmd
|
|
529
|
+
.command("verify <path>")
|
|
530
|
+
.description("Confirm a path is a worktree of this repo, not a foreign or stale one (exit 1 if not)")
|
|
531
|
+
.option("--issue <issue>", "Also require the branch to belong to this issue")
|
|
532
|
+
.option("--json", "Output as JSON")
|
|
533
|
+
.action(worktreeVerifyCommand);
|
|
489
534
|
// Auto-sync skills after npm upgrade (version mismatch detection)
|
|
490
535
|
// Only triggers when skills were previously synced (has .sequant-version marker).
|
|
491
536
|
// Projects that manage skills manually (no marker) are not affected.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `sequant locks` — inspect and clear per-issue concurrency locks (#625)
|
|
2
|
+
* `sequant locks` — inspect and clear per-issue concurrency locks (#625) and
|
|
3
|
+
* the checkout-scoped lock (#901).
|
|
3
4
|
*/
|
|
4
5
|
export interface LocksListOptions {
|
|
5
6
|
json?: boolean;
|
|
@@ -26,6 +27,24 @@ export interface LocksCheckBatchOptions {
|
|
|
26
27
|
}
|
|
27
28
|
/** `sequant locks list` — print every active lock with staleness metadata. */
|
|
28
29
|
export declare function locksListCommand(options?: LocksListOptions): Promise<void>;
|
|
30
|
+
export interface LocksCheckoutOptions {
|
|
31
|
+
issue?: string;
|
|
32
|
+
command?: string;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
skipPidCheck?: boolean;
|
|
35
|
+
force?: boolean;
|
|
36
|
+
json?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `sequant locks checkout <acquire|release|check|clear>` — the working-tree
|
|
40
|
+
* lock (#901).
|
|
41
|
+
*
|
|
42
|
+
* Exit codes mirror the per-issue commands:
|
|
43
|
+
* 0 — success (acquired / released / free / cleared / nothing to release)
|
|
44
|
+
* 1 — held by another session, or refused
|
|
45
|
+
* 2 — invalid arguments
|
|
46
|
+
*/
|
|
47
|
+
export declare function locksCheckoutCommand(action: string, options?: LocksCheckoutOptions): Promise<void>;
|
|
29
48
|
/**
|
|
30
49
|
* `sequant locks clear <issue>` — remove a lock manually.
|
|
31
50
|
* By default refuses to clear a fresh same-host lock whose PID is alive;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `sequant locks` — inspect and clear per-issue concurrency locks (#625)
|
|
2
|
+
* `sequant locks` — inspect and clear per-issue concurrency locks (#625) and
|
|
3
|
+
* the checkout-scoped lock (#901).
|
|
3
4
|
*/
|
|
4
5
|
import chalk from "chalk";
|
|
5
|
-
import { LockManager, formatLockedMessage, } from "../lib/locks/index.js";
|
|
6
|
+
import { CheckoutLock, LockManager, describeCheckoutHolderIssue, formatCheckoutLockedMessage, formatLockedMessage, } from "../lib/locks/index.js";
|
|
6
7
|
/** Human-readable line for the `--signal-other` log output (#637). */
|
|
7
8
|
function formatSignalLine(issue, pid, result) {
|
|
8
9
|
switch (result.reason) {
|
|
@@ -44,14 +45,35 @@ export async function locksListCommand(options = {}) {
|
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
const listings = manager.list();
|
|
48
|
+
// The checkout lock lives in the same directory but is deliberately not a
|
|
49
|
+
// numeric filename, so `manager.list()` skips it (#901). Query it separately
|
|
50
|
+
// rather than widening the numeric key everywhere.
|
|
51
|
+
const checkout = new CheckoutLock().listing();
|
|
47
52
|
if (options.json) {
|
|
48
|
-
console.log(JSON.stringify({ locks: listings }, null, 2));
|
|
53
|
+
console.log(JSON.stringify({ locks: listings, checkout }, null, 2));
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
51
|
-
if (listings.length === 0) {
|
|
56
|
+
if (listings.length === 0 && !checkout) {
|
|
52
57
|
console.log(chalk.gray("No active locks."));
|
|
53
58
|
return;
|
|
54
59
|
}
|
|
60
|
+
if (checkout) {
|
|
61
|
+
const ageMinutes = Math.floor(checkout.ageMs / 60_000);
|
|
62
|
+
const staleTag = checkout.stale
|
|
63
|
+
? chalk.yellow(` (stale: ${checkout.staleReason})`)
|
|
64
|
+
: "";
|
|
65
|
+
console.log(chalk.bold("Checkout lock (whole working tree):"));
|
|
66
|
+
console.log(` issue=${describeCheckoutHolderIssue(checkout.holder.issue)} ` +
|
|
67
|
+
`pid=${checkout.holder.pid} ` +
|
|
68
|
+
`host=${checkout.holder.hostname} age=${ageMinutes}m ` +
|
|
69
|
+
`started=${checkout.holder.startedAt}${staleTag}`);
|
|
70
|
+
console.log(` command: ${checkout.holder.command}`);
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
if (listings.length === 0) {
|
|
74
|
+
console.log(chalk.gray("No active per-issue locks."));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
55
77
|
console.log(chalk.bold(`Active locks (${listings.length}):`));
|
|
56
78
|
console.log("");
|
|
57
79
|
for (const l of listings) {
|
|
@@ -62,6 +84,186 @@ export async function locksListCommand(options = {}) {
|
|
|
62
84
|
console.log(` command: ${l.holder.command}`);
|
|
63
85
|
}
|
|
64
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate `--issue` for the checkout commands. Shared by `acquire` (where the
|
|
89
|
+
* flag is required) and `release` (where it is optional) so the two cannot
|
|
90
|
+
* drift on what counts as a valid issue.
|
|
91
|
+
*
|
|
92
|
+
* `Number.parseInt` + `Number.isInteger`, never a bare `Number(x)`:
|
|
93
|
+
* `Number(undefined)` is `NaN`, and `NaN !== undefined`, so a `NaN` would sail
|
|
94
|
+
* through the `issue !== undefined` guard in `isCheckoutOwner` and silently
|
|
95
|
+
* refuse every release instead of erroring here (#906).
|
|
96
|
+
*/
|
|
97
|
+
function parseCheckoutIssue(raw) {
|
|
98
|
+
const issue = Number.parseInt(raw, 10);
|
|
99
|
+
if (!Number.isInteger(issue) || issue <= 0) {
|
|
100
|
+
console.error(chalk.red(`Invalid issue number: ${raw}`));
|
|
101
|
+
process.exitCode = 2;
|
|
102
|
+
return { ok: false };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, issue };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `sequant locks checkout <acquire|release|check|clear>` — the working-tree
|
|
108
|
+
* lock (#901).
|
|
109
|
+
*
|
|
110
|
+
* Exit codes mirror the per-issue commands:
|
|
111
|
+
* 0 — success (acquired / released / free / cleared / nothing to release)
|
|
112
|
+
* 1 — held by another session, or refused
|
|
113
|
+
* 2 — invalid arguments
|
|
114
|
+
*/
|
|
115
|
+
export async function locksCheckoutCommand(action, options = {}) {
|
|
116
|
+
const lock = new CheckoutLock();
|
|
117
|
+
if (lock.isNoop) {
|
|
118
|
+
// AC-5: orchestrator/MCP mode is a no-op across the whole surface.
|
|
119
|
+
if (options.json) {
|
|
120
|
+
console.log(JSON.stringify({ action, orchestratorMode: true, ok: true }));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
switch (action) {
|
|
128
|
+
case "acquire": {
|
|
129
|
+
if (options.issue === undefined) {
|
|
130
|
+
console.error(chalk.red("`locks checkout acquire` requires --issue"));
|
|
131
|
+
process.exitCode = 2;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const parsed = parseCheckoutIssue(options.issue);
|
|
135
|
+
if (!parsed.ok)
|
|
136
|
+
return;
|
|
137
|
+
const issue = parsed.issue;
|
|
138
|
+
const result = lock.acquire(issue, options.command ?? "unknown", {
|
|
139
|
+
sessionId: options.sessionId,
|
|
140
|
+
skipPidCheck: options.skipPidCheck,
|
|
141
|
+
});
|
|
142
|
+
if (result.acquired) {
|
|
143
|
+
if (options.json) {
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
acquired: true,
|
|
146
|
+
reentrant: result.reentrant,
|
|
147
|
+
lockPath: result.lockPath,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(chalk.green(result.reentrant
|
|
152
|
+
? `✓ Checkout already held by this session (#${issue})`
|
|
153
|
+
: `✓ Acquired checkout lock for #${issue}`));
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
if (options.json) {
|
|
159
|
+
console.log(JSON.stringify({
|
|
160
|
+
acquired: false,
|
|
161
|
+
holder: result.holder,
|
|
162
|
+
lockPath: result.lockPath,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.error(chalk.yellow(formatCheckoutLockedMessage(result.holder, { issue })));
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
case "release": {
|
|
171
|
+
// `--issue` is optional here, unlike `acquire`: a live process releasing
|
|
172
|
+
// its own lock is identified by PID. It is required in practice for
|
|
173
|
+
// skill shells, whose PID is already gone — see `isCheckoutOwner`.
|
|
174
|
+
let issue;
|
|
175
|
+
if (options.issue !== undefined) {
|
|
176
|
+
const parsed = parseCheckoutIssue(options.issue);
|
|
177
|
+
if (!parsed.ok)
|
|
178
|
+
return;
|
|
179
|
+
issue = parsed.issue;
|
|
180
|
+
}
|
|
181
|
+
// Read the holder *before* releasing, so a refusal can name it.
|
|
182
|
+
const holder = lock.check();
|
|
183
|
+
const released = lock.release({
|
|
184
|
+
sessionId: options.sessionId,
|
|
185
|
+
...lock.selfIdentity,
|
|
186
|
+
issue,
|
|
187
|
+
});
|
|
188
|
+
// Three outcomes, not two (#906). Before ownership was enforced,
|
|
189
|
+
// "released nothing" could only mean "nothing was held". It now also
|
|
190
|
+
// means "held, but not by you" — a real refusal, which must not print
|
|
191
|
+
// the same gray no-op line or exit 0.
|
|
192
|
+
if (options.json) {
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
released,
|
|
195
|
+
refused: !released && holder !== null,
|
|
196
|
+
...(holder ? { holder } : {}),
|
|
197
|
+
}));
|
|
198
|
+
if (!released && holder)
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (released) {
|
|
203
|
+
console.log(chalk.green("✓ Released checkout lock"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (holder) {
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
console.error(chalk.yellow(`Refusing to release the checkout lock — it belongs to the session working ${describeCheckoutHolderIssue(holder.issue)} ` +
|
|
209
|
+
`(PID ${holder.pid} on ${holder.hostname}, started ${holder.startedAt}).\n` +
|
|
210
|
+
(issue === undefined
|
|
211
|
+
? `You passed no --issue, so nothing identified you as the holder.\n` +
|
|
212
|
+
` • If you are that session: sequant locks checkout release --issue=${holder.issue}\n`
|
|
213
|
+
: `You passed --issue=${issue}.\n`) +
|
|
214
|
+
` • If that session is gone: sequant locks checkout clear --force`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
console.log(chalk.gray("No releasable checkout lock"));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
case "check": {
|
|
221
|
+
const holder = lock.check();
|
|
222
|
+
if (!holder) {
|
|
223
|
+
if (options.json) {
|
|
224
|
+
console.log(JSON.stringify({ locked: false }));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
console.log(chalk.gray("Checkout is not locked"));
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
if (options.json) {
|
|
233
|
+
console.log(JSON.stringify({ locked: true, holder }));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(chalk.yellow(formatCheckoutLockedMessage(holder)));
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "clear": {
|
|
241
|
+
const result = lock.clear({ safetyCheck: !options.force });
|
|
242
|
+
if (options.json) {
|
|
243
|
+
console.log(JSON.stringify(result));
|
|
244
|
+
if (!result.cleared)
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (result.cleared) {
|
|
249
|
+
console.log(chalk.green("✓ Cleared checkout lock"));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
if (result.reason === "no-lock") {
|
|
254
|
+
console.log(chalk.gray("No checkout lock to clear"));
|
|
255
|
+
process.exitCode = 0;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(chalk.yellow("Refusing to clear a fresh checkout lock. " +
|
|
259
|
+
"Re-run with `sequant locks checkout clear --force` if you are sure the holder is gone."));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
console.error(chalk.red(`Unknown action: ${action}. Expected acquire|release|check|clear.`));
|
|
264
|
+
process.exitCode = 2;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
65
267
|
/**
|
|
66
268
|
* `sequant locks clear <issue>` — remove a lock manually.
|
|
67
269
|
* By default refuses to clear a fresh same-host lock whose PID is alive;
|
|
@@ -25,6 +25,12 @@ export interface ReadyCommandOptions {
|
|
|
25
25
|
mcp?: boolean;
|
|
26
26
|
json?: boolean;
|
|
27
27
|
verbose?: boolean;
|
|
28
|
+
/** Per-phase model override (#914). See `RunOptions.models`. */
|
|
29
|
+
models?: string;
|
|
30
|
+
/** Per-phase effort override (#914). See `RunOptions.efforts`. */
|
|
31
|
+
efforts?: string;
|
|
32
|
+
/** Evidence-based effort escalation on QA-pass retries (#915). See `RunOptions.escalateEffort`. */
|
|
33
|
+
escalateEffort?: boolean;
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* Exit code from a ready result.
|
|
@@ -23,7 +23,8 @@ import { executePhaseWithRetry } from "../lib/workflow/phase-executor.js";
|
|
|
23
23
|
import { buildProgressWiring } from "./run-progress.js";
|
|
24
24
|
import { ReadySnapshotAdapter } from "./ready-tui-adapter.js";
|
|
25
25
|
import { DEFAULT_CONFIG } from "../lib/workflow/types.js";
|
|
26
|
-
import { positiveOr } from "../lib/workflow/config-resolver.js";
|
|
26
|
+
import { positiveOr, resolvePhasePolicies, } from "../lib/workflow/config-resolver.js";
|
|
27
|
+
import { getPhaseNames } from "../lib/workflow/phase-registry.js";
|
|
27
28
|
import { runReadyGate, parseNonGoals, } from "../lib/workflow/ready-gate.js";
|
|
28
29
|
/**
|
|
29
30
|
* Exit code from a ready result.
|
|
@@ -106,6 +107,10 @@ export async function readyCommand(issueArg, options) {
|
|
|
106
107
|
const policy = resolvePolicy(options.policy, settings.ready.policy);
|
|
107
108
|
const { maxIterations, tokenBudget, phaseTimeout } = resolveReadyLimits(options, settings);
|
|
108
109
|
const mcp = options.mcp !== false;
|
|
110
|
+
const phasePolicies = resolvePhasePolicies(options.models, options.efforts, settings.run.phases, getPhaseNames());
|
|
111
|
+
// #915: CLI > settings > default `false`, same precedence as the `run`
|
|
112
|
+
// path's `buildExecutionConfig` (config-resolver.ts).
|
|
113
|
+
const effortEscalation = options.escalateEffort ?? settings.run.effortEscalation ?? false;
|
|
109
114
|
// Resolve the issue's existing worktree (reuses run/state worktree infra).
|
|
110
115
|
const worktreePath = resolveWorktreePath(issueNumber);
|
|
111
116
|
if (!worktreePath) {
|
|
@@ -198,6 +203,8 @@ export async function readyCommand(issueArg, options) {
|
|
|
198
203
|
verbose: options.verbose,
|
|
199
204
|
runPhase,
|
|
200
205
|
onProgress,
|
|
206
|
+
phasePolicies,
|
|
207
|
+
effortEscalation,
|
|
201
208
|
});
|
|
202
209
|
}
|
|
203
210
|
catch (error) {
|
|
@@ -220,6 +227,13 @@ export async function readyCommand(issueArg, options) {
|
|
|
220
227
|
process.exitCode = 2;
|
|
221
228
|
return;
|
|
222
229
|
}
|
|
230
|
+
// #915: surface any escalated qa/loop dispatches — the gate has no live
|
|
231
|
+
// print of its own (see `withEscalatedEffort` in `ready-gate.ts`).
|
|
232
|
+
if (options.verbose) {
|
|
233
|
+
for (const e of result.effortEscalations) {
|
|
234
|
+
console.log(colors.muted(` effort: ${e.base} → ${e.escalated} (${e.phase} retry)`));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
223
237
|
// Persist the terminal state so `sequant status` reflects it (Derived AC).
|
|
224
238
|
// Best-effort: initialize the issue in state if a prior run didn't track it.
|
|
225
239
|
try {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant worktree` — repo-scoped worktree resolution for skill bodies (#899).
|
|
3
|
+
*
|
|
4
|
+
* `/fullsolve` uses `resolve` to turn an issue number into a real absolute
|
|
5
|
+
* path before exporting `SEQUANT_WORKTREE`; `/exec` uses `verify` to refuse a
|
|
6
|
+
* path that does not exist or belongs to another repository. Both fail closed:
|
|
7
|
+
* a non-zero exit means the caller must halt, not continue where it stands.
|
|
8
|
+
*/
|
|
9
|
+
export interface WorktreeResolveOptions {
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface WorktreeVerifyOptions {
|
|
13
|
+
issue?: string;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `sequant worktree resolve <issue>` — print the absolute path of this
|
|
18
|
+
* repository's worktree for an issue.
|
|
19
|
+
*
|
|
20
|
+
* On success the path is the *only* thing written to stdout, so callers can
|
|
21
|
+
* capture it with `WT="$(sequant worktree resolve 123)"`. Everything else goes
|
|
22
|
+
* to stderr.
|
|
23
|
+
*/
|
|
24
|
+
export declare function worktreeResolveCommand(issueArg: string, options?: WorktreeResolveOptions): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* `sequant worktree verify <path>` — confirm a path is a worktree of this
|
|
27
|
+
* repository (and, with `--issue`, that it belongs to that issue).
|
|
28
|
+
*
|
|
29
|
+
* Exit 1 with a named error is the signal for `/exec` to halt.
|
|
30
|
+
*/
|
|
31
|
+
export declare function worktreeVerifyCommand(pathArg: string, options?: WorktreeVerifyOptions): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant worktree` — repo-scoped worktree resolution for skill bodies (#899).
|
|
3
|
+
*
|
|
4
|
+
* `/fullsolve` uses `resolve` to turn an issue number into a real absolute
|
|
5
|
+
* path before exporting `SEQUANT_WORKTREE`; `/exec` uses `verify` to refuse a
|
|
6
|
+
* path that does not exist or belongs to another repository. Both fail closed:
|
|
7
|
+
* a non-zero exit means the caller must halt, not continue where it stands.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { resolveIssueWorktree, verifyWorktreePath, } from "../lib/workflow/worktree-resolver.js";
|
|
11
|
+
function parseIssue(arg) {
|
|
12
|
+
const issue = Number.parseInt(arg, 10);
|
|
13
|
+
if (!Number.isInteger(issue) || issue <= 0) {
|
|
14
|
+
console.error(chalk.red(`Invalid issue number: ${arg}`));
|
|
15
|
+
process.exitCode = 2;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return issue;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `sequant worktree resolve <issue>` — print the absolute path of this
|
|
22
|
+
* repository's worktree for an issue.
|
|
23
|
+
*
|
|
24
|
+
* On success the path is the *only* thing written to stdout, so callers can
|
|
25
|
+
* capture it with `WT="$(sequant worktree resolve 123)"`. Everything else goes
|
|
26
|
+
* to stderr.
|
|
27
|
+
*/
|
|
28
|
+
export async function worktreeResolveCommand(issueArg, options = {}) {
|
|
29
|
+
const issue = parseIssue(issueArg);
|
|
30
|
+
if (issue === null)
|
|
31
|
+
return;
|
|
32
|
+
const result = resolveIssueWorktree(issue);
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
if (options.json) {
|
|
36
|
+
console.error(JSON.stringify({
|
|
37
|
+
issue,
|
|
38
|
+
ok: false,
|
|
39
|
+
error: result.error,
|
|
40
|
+
message: result.message,
|
|
41
|
+
candidates: result.candidates,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.error(chalk.red(result.message));
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify({
|
|
51
|
+
issue,
|
|
52
|
+
ok: true,
|
|
53
|
+
path: result.path,
|
|
54
|
+
branch: result.branch,
|
|
55
|
+
}));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Bare path only — this is consumed by `$(...)` in skill bodies.
|
|
59
|
+
console.log(result.path);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `sequant worktree verify <path>` — confirm a path is a worktree of this
|
|
63
|
+
* repository (and, with `--issue`, that it belongs to that issue).
|
|
64
|
+
*
|
|
65
|
+
* Exit 1 with a named error is the signal for `/exec` to halt.
|
|
66
|
+
*/
|
|
67
|
+
export async function worktreeVerifyCommand(pathArg, options = {}) {
|
|
68
|
+
let issue;
|
|
69
|
+
if (options.issue !== undefined) {
|
|
70
|
+
const parsed = parseIssue(options.issue);
|
|
71
|
+
if (parsed === null)
|
|
72
|
+
return;
|
|
73
|
+
issue = parsed;
|
|
74
|
+
}
|
|
75
|
+
const result = verifyWorktreePath(pathArg, { issue });
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
if (options.json) {
|
|
79
|
+
console.error(JSON.stringify({
|
|
80
|
+
ok: false,
|
|
81
|
+
error: result.error,
|
|
82
|
+
message: result.message,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.error(chalk.red(result.message));
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (options.json) {
|
|
91
|
+
console.log(JSON.stringify({ ok: true, path: result.path, branch: result.branch }));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.log(chalk.green(`✓ ${result.path} (${result.branch})`));
|
|
95
|
+
}
|
|
@@ -31,3 +31,26 @@ export declare function parseWholeNumber(flag: string, opts: WholeNumberOptions)
|
|
|
31
31
|
* `merge --watch` messages this originally shipped with do not change.
|
|
32
32
|
*/
|
|
33
33
|
export declare function parsePositiveSeconds(flag: string): (value: string) => number;
|
|
34
|
+
/**
|
|
35
|
+
* Build a commander coercion for a `--models`/`--efforts`-shaped flag (#914).
|
|
36
|
+
*
|
|
37
|
+
* Validates the spec via `config-resolver.ts`'s `parsePhaseSpec` — a bare
|
|
38
|
+
* value applies to every phase, a comma list of `phase=value` pairs applies
|
|
39
|
+
* per phase, and a malformed spec (empty segment, mixed bare/pair form, or
|
|
40
|
+
* an unrecognized phase name) fails fast here rather than reaching
|
|
41
|
+
* `resolvePhasePolicies` silently. On success, returns the ORIGINAL raw spec
|
|
42
|
+
* string unchanged (not the parsed map) — `RunOptions.models`/`.efforts`
|
|
43
|
+
* stay plain strings, and `resolvePhasePolicies` re-parses for real when
|
|
44
|
+
* building `ExecutionConfig.phasePolicies`, so parsing has exactly one
|
|
45
|
+
* source of truth even though it runs twice.
|
|
46
|
+
*
|
|
47
|
+
* `allowedValues`, when given, additionally rejects any resolved value not
|
|
48
|
+
* in the set — used for `--efforts` (closed enum: `settings.ts:EFFORT_LEVELS`)
|
|
49
|
+
* so a typo like `--efforts exec=mediu` fails at the CLI boundary the same
|
|
50
|
+
* way it already does when set via `settings.json`, instead of only
|
|
51
|
+
* surfacing once the value reaches the SDK. `--models` passes no
|
|
52
|
+
* `allowedValues` — model aliases/IDs are intentionally unvalidated (they
|
|
53
|
+
* churn independently of sequant releases; the SDK errors clearly on a bad
|
|
54
|
+
* one).
|
|
55
|
+
*/
|
|
56
|
+
export declare function parsePhaseSpecFlag(flag: string, phaseNames: string[], allowedValues?: readonly string[]): (value: string) => string;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import { InvalidArgumentError } from "commander";
|
|
11
|
+
import { parsePhaseSpec } from "./workflow/config-resolver.js";
|
|
11
12
|
/**
|
|
12
13
|
* Build a commander coercion for a whole-number flag.
|
|
13
14
|
*
|
|
@@ -44,3 +45,45 @@ export function parsePositiveSeconds(flag) {
|
|
|
44
45
|
unitSingular: "second",
|
|
45
46
|
});
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a commander coercion for a `--models`/`--efforts`-shaped flag (#914).
|
|
50
|
+
*
|
|
51
|
+
* Validates the spec via `config-resolver.ts`'s `parsePhaseSpec` — a bare
|
|
52
|
+
* value applies to every phase, a comma list of `phase=value` pairs applies
|
|
53
|
+
* per phase, and a malformed spec (empty segment, mixed bare/pair form, or
|
|
54
|
+
* an unrecognized phase name) fails fast here rather than reaching
|
|
55
|
+
* `resolvePhasePolicies` silently. On success, returns the ORIGINAL raw spec
|
|
56
|
+
* string unchanged (not the parsed map) — `RunOptions.models`/`.efforts`
|
|
57
|
+
* stay plain strings, and `resolvePhasePolicies` re-parses for real when
|
|
58
|
+
* building `ExecutionConfig.phasePolicies`, so parsing has exactly one
|
|
59
|
+
* source of truth even though it runs twice.
|
|
60
|
+
*
|
|
61
|
+
* `allowedValues`, when given, additionally rejects any resolved value not
|
|
62
|
+
* in the set — used for `--efforts` (closed enum: `settings.ts:EFFORT_LEVELS`)
|
|
63
|
+
* so a typo like `--efforts exec=mediu` fails at the CLI boundary the same
|
|
64
|
+
* way it already does when set via `settings.json`, instead of only
|
|
65
|
+
* surfacing once the value reaches the SDK. `--models` passes no
|
|
66
|
+
* `allowedValues` — model aliases/IDs are intentionally unvalidated (they
|
|
67
|
+
* churn independently of sequant releases; the SDK errors clearly on a bad
|
|
68
|
+
* one).
|
|
69
|
+
*/
|
|
70
|
+
export function parsePhaseSpecFlag(flag, phaseNames, allowedValues) {
|
|
71
|
+
return (value) => {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = parsePhaseSpec(value, phaseNames);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
throw new InvalidArgumentError(`${flag}: ${message}`);
|
|
79
|
+
}
|
|
80
|
+
if (allowedValues) {
|
|
81
|
+
for (const v of Object.values(parsed)) {
|
|
82
|
+
if (!allowedValues.includes(v)) {
|
|
83
|
+
throw new InvalidArgumentError(`${flag}: '${v}' is not one of ${allowedValues.join("|")}.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -117,6 +117,8 @@ export interface IssueSummary {
|
|
|
117
117
|
loopTriggered?: boolean;
|
|
118
118
|
prNumber?: number;
|
|
119
119
|
prUrl?: string;
|
|
120
|
+
/** Set when PR creation was skipped for lack of commits ahead of base (#920). */
|
|
121
|
+
prSkippedReason?: string;
|
|
120
122
|
failureReason?: string;
|
|
121
123
|
qaVerdict?: string;
|
|
122
124
|
unmetCount?: number;
|