sequant 2.10.0 → 2.12.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 +19 -2
- package/dist/bin/cli.js +47 -2
- 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/hooks/pre-tool.sh +331 -12
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
- package/dist/src/commands/doctor.js +20 -18
- 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 +19 -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/ac-linter.js +26 -0
- package/dist/src/lib/ac-parser.d.ts +40 -0
- package/dist/src/lib/ac-parser.js +202 -16
- 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/markdown-fence.d.ts +24 -0
- package/dist/src/lib/markdown-fence.js +51 -0
- package/dist/src/lib/mcp-config.d.ts +24 -0
- package/dist/src/lib/mcp-config.js +51 -0
- package/dist/src/lib/scope/analyzer.d.ts +4 -0
- package/dist/src/lib/scope/analyzer.js +7 -1
- package/dist/src/lib/settings.d.ts +111 -1
- package/dist/src/lib/settings.js +59 -0
- package/dist/src/lib/system.d.ts +7 -3
- package/dist/src/lib/system.js +7 -3
- package/dist/src/lib/test-tautology-detector.d.ts +4 -3
- package/dist/src/lib/test-tautology-detector.js +147 -40
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +154 -23
- package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
- package/dist/src/lib/workflow/config-resolver.js +90 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +14 -3
- 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/mutation-marker.d.ts +86 -0
- package/dist/src/lib/workflow/mutation-marker.js +97 -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 +17 -0
- package/dist/src/lib/workflow/phase-executor.js +60 -4
- package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
- package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +53 -1
- package/dist/src/lib/workflow/ready-gate.js +105 -14
- package/dist/src/lib/workflow/run-log-schema.d.ts +175 -0
- package/dist/src/lib/workflow/run-log-schema.js +71 -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/state-schema.d.ts +5 -1
- package/dist/src/lib/workflow/state-schema.js +8 -1
- package/dist/src/lib/workflow/types.d.ts +78 -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 +4 -3
- package/templates/hooks/pre-tool.sh +331 -12
- 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 +62 -8
- package/templates/skills/fullsolve/SKILL.md +187 -28
- package/templates/skills/loop/SKILL.md +127 -23
- package/templates/skills/merger/SKILL.md +130 -13
- package/templates/skills/qa/SKILL.md +306 -8
- package/templates/skills/release/SKILL.md +79 -0
- package/templates/skills/spec/SKILL.md +40 -20
- 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/templates/agents/sequant-explorer.md +0 -24
package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Recommended Workflow Format
|
|
2
2
|
|
|
3
|
-
This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output.
|
|
3
|
+
This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output.
|
|
4
|
+
|
|
5
|
+
## Resolution chain (#921)
|
|
6
|
+
|
|
7
|
+
`sequant run` resolves phases through an ordered chain, not `parseRecommendedWorkflow()` alone:
|
|
8
|
+
|
|
9
|
+
1. **`SEQUANT_SPEC` marker** — a structured HTML comment in the posted plan comment, e.g. `<!-- SEQUANT_SPEC: {"phases":["testgen","exec","qa"],"qualityLoop":true} -->`. This is the primary, durable channel — always emit it alongside the prose section below.
|
|
10
|
+
2. **Comment prose** — `parseRecommendedWorkflow()` applied to the plan comment body (same format as this doc).
|
|
11
|
+
3. **Chat text** — the same parser applied to the spec agent's chat output. Nondeterministic: only present if the agent happens to restate the section in chat rather than posting via a body file (#814).
|
|
12
|
+
4. **Label fallback** — `detectPhasesFromLabels()`. Can never produce `testgen` or `security-review`.
|
|
13
|
+
|
|
14
|
+
The marker's `phases` array excludes `spec` (it already ran) and must name only registered phases — an unknown phase name invalidates the whole marker and falls through to step 2.
|
|
4
15
|
|
|
5
16
|
## Format
|
|
6
17
|
|
|
@@ -10,6 +21,8 @@ This document shows the expected output format for the `## Recommended Workflow`
|
|
|
10
21
|
**Phases:** spec → exec → qa
|
|
11
22
|
**Quality Loop:** disabled
|
|
12
23
|
**Reasoning:** Brief explanation of why this workflow was chosen.
|
|
24
|
+
|
|
25
|
+
<!-- SEQUANT_SPEC: {"phases":["exec","qa"],"qualityLoop":false} -->
|
|
13
26
|
```
|
|
14
27
|
|
|
15
28
|
## Examples
|
|
@@ -582,7 +582,7 @@ Create structured test results:
|
|
|
582
582
|
### 3.2 GitHub Comment
|
|
583
583
|
|
|
584
584
|
**If orchestrated (SEQUANT_ORCHESTRATOR is set):**
|
|
585
|
-
- Skip posting GitHub comment
|
|
585
|
+
- Skip posting this skill's own GitHub comment — no per-phase comment is posted under `sequant run`; test results surface through the run summary and the PR body (#964)
|
|
586
586
|
- Include test summary in output for orchestrator to capture
|
|
587
587
|
- Let orchestrator aggregate results across phases
|
|
588
588
|
|
|
@@ -6,6 +6,7 @@ metadata:
|
|
|
6
6
|
author: sequant
|
|
7
7
|
version: "1.0"
|
|
8
8
|
allowed-tools:
|
|
9
|
+
- Bash(npx sequant worktree:*)
|
|
9
10
|
- Read
|
|
10
11
|
- Write
|
|
11
12
|
- Edit
|
|
@@ -530,17 +531,33 @@ If an AC has verification method "N/A - Trivial", skip test generation and note
|
|
|
530
531
|
|
|
531
532
|
### Step 4: Locate Feature Worktree
|
|
532
533
|
|
|
533
|
-
If generating file-based tests (Unit Test, Integration Test), find the worktree
|
|
534
|
+
If generating file-based tests (Unit Test, Integration Test), find the worktree.
|
|
534
535
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
<!-- BEGIN: worktree-standalone-lookup (#899) -->
|
|
537
|
+
|
|
538
|
+
Resolve it through git, not the filesystem:
|
|
538
539
|
|
|
539
|
-
Or check:
|
|
540
540
|
```bash
|
|
541
|
-
|
|
541
|
+
WORKTREE="$(npx sequant worktree resolve <issue-number>)" || {
|
|
542
|
+
echo "❌ HALT: no worktree for #<issue-number> in this repository."
|
|
543
|
+
exit 1
|
|
544
|
+
}
|
|
545
|
+
cd "$WORKTREE"
|
|
542
546
|
```
|
|
543
547
|
|
|
548
|
+
`sequant worktree resolve` reads `git worktree list` in the current repository
|
|
549
|
+
— which reports only *this* repo's worktrees — and selects on the **branch**
|
|
550
|
+
git reports, not the directory name.
|
|
551
|
+
|
|
552
|
+
**Do not glob `../worktrees/feature/<issue-number>-*`, and do not grep
|
|
553
|
+
`git worktree list` for the issue number.** The first matches across sibling
|
|
554
|
+
repositories, which share that directory; the second matches the printed path,
|
|
555
|
+
so it keys on the directory slug — and a slug can drift from its own branch
|
|
556
|
+
after a rename. Because this skill **writes test files**, landing in the wrong
|
|
557
|
+
tree scatters stubs into an unrelated project.
|
|
558
|
+
|
|
559
|
+
<!-- END: worktree-standalone-lookup (#899) -->
|
|
560
|
+
|
|
544
561
|
Create test directories if needed:
|
|
545
562
|
```bash
|
|
546
563
|
mkdir -p __tests__/integration
|
|
@@ -8,7 +8,8 @@ import { GitHubProvider } from "../lib/workflow/platforms/github.js";
|
|
|
8
8
|
import { fileExists, isExecutable } from "../lib/fs.js";
|
|
9
9
|
import { checkSkillsInstalled } from "../lib/skills-check.js";
|
|
10
10
|
import { getManifest } from "../lib/manifest.js";
|
|
11
|
-
import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers,
|
|
11
|
+
import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
|
|
12
|
+
import { getPhaseMcpServersConfig } from "../lib/mcp-config.js";
|
|
12
13
|
import { getSettings, DEFAULT_AGENT_SETTINGS } from "../lib/settings.js";
|
|
13
14
|
import { checkVersionThorough, getVersionWarning, resolveCliInvocation, } from "../lib/version-check.js";
|
|
14
15
|
import { areSkillsOutdated } from "./sync.js";
|
|
@@ -435,23 +436,24 @@ export async function doctorCommand(options = {}) {
|
|
|
435
436
|
message: "No optional MCPs configured (Sequant works without them, but they enhance functionality)",
|
|
436
437
|
});
|
|
437
438
|
}
|
|
438
|
-
// Check: MCP availability for headless mode (sequant run)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
439
|
+
// Check: MCP availability for headless mode (sequant run) (#936)
|
|
440
|
+
//
|
|
441
|
+
// Phase agents read the project's .mcp.json + settings.run.mcpAllowlist,
|
|
442
|
+
// never Claude Desktop config wholesale — see getPhaseMcpServersConfig.
|
|
443
|
+
// The sequant server is always guaranteed, so this check reports what a
|
|
444
|
+
// phase will actually receive rather than pass/warn on presence.
|
|
445
|
+
const phaseServersConfig = getPhaseMcpServersConfig(process.cwd(), {
|
|
446
|
+
desktopAllowlist: settings.run.mcpAllowlist,
|
|
447
|
+
});
|
|
448
|
+
const phaseServerCount = Object.keys(phaseServersConfig).length;
|
|
449
|
+
const extraServerCount = phaseServerCount - 1; // minus the guaranteed sequant entry
|
|
450
|
+
checks.push({
|
|
451
|
+
name: "MCP Servers (headless)",
|
|
452
|
+
status: "pass",
|
|
453
|
+
message: extraServerCount > 0
|
|
454
|
+
? `Available for sequant run (${phaseServerCount} servers: sequant + ${extraServerCount} from .mcp.json${settings.run.mcpAllowlist?.length ? "/mcpAllowlist" : ""})`
|
|
455
|
+
: "Available for sequant run (sequant only — add servers to .mcp.json, or settings.run.mcpAllowlist for desktop servers, for more)",
|
|
456
|
+
});
|
|
455
457
|
// Check: Sequant MCP server health
|
|
456
458
|
try {
|
|
457
459
|
// Verify MCP server can be created (validates SDK availability)
|
|
@@ -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) {
|
|
@@ -195,9 +200,15 @@ export async function readyCommand(issueArg, options) {
|
|
|
195
200
|
nonGoals,
|
|
196
201
|
phaseTimeout,
|
|
197
202
|
mcp,
|
|
203
|
+
mcpAllowlist: settings.run.mcpAllowlist,
|
|
198
204
|
verbose: options.verbose,
|
|
199
205
|
runPhase,
|
|
200
206
|
onProgress,
|
|
207
|
+
phasePolicies,
|
|
208
|
+
effortEscalation,
|
|
209
|
+
// #937 AC-4: persist the final gap report so it survives the terminal
|
|
210
|
+
// closing (previously terminal-scrollback only under `ac` policy).
|
|
211
|
+
postReport: (body) => gh.postComment(String(issueNumber), body),
|
|
201
212
|
});
|
|
202
213
|
}
|
|
203
214
|
catch (error) {
|
|
@@ -220,6 +231,13 @@ export async function readyCommand(issueArg, options) {
|
|
|
220
231
|
process.exitCode = 2;
|
|
221
232
|
return;
|
|
222
233
|
}
|
|
234
|
+
// #915: surface any escalated qa/loop dispatches — the gate has no live
|
|
235
|
+
// print of its own (see `withEscalatedEffort` in `ready-gate.ts`).
|
|
236
|
+
if (options.verbose) {
|
|
237
|
+
for (const e of result.effortEscalations) {
|
|
238
|
+
console.log(colors.muted(` effort: ${e.base} → ${e.escalated} (${e.phase} retry)`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
223
241
|
// Persist the terminal state so `sequant status` reflects it (Derived AC).
|
|
224
242
|
// Best-effort: initialize the issue in state if a prior run didn't track it.
|
|
225
243
|
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
|
+
}
|
|
@@ -272,6 +272,29 @@ function detectTitleBodyTension(ac) {
|
|
|
272
272
|
suggestion: "Two verification bars detected. Either (a) tighten the title to match the runtime body (e.g., 'Smoke test execution — capture evidence'), or (b) split the runtime requirement into a separate AC.",
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Detect a test-type AC (unit/integration/browser) whose verification
|
|
277
|
+
* method came from keyword inference rather than a declared `Evidence:`
|
|
278
|
+
* clause (#938). `manual` ACs are exempt — docs/decision ACs legitimately
|
|
279
|
+
* have no runnable evidence.
|
|
280
|
+
*
|
|
281
|
+
* Warning-only, same convention as the regex-based DEFAULT_LINT_PATTERNS.
|
|
282
|
+
*
|
|
283
|
+
* @param ac - The acceptance criterion to check
|
|
284
|
+
* @returns A lint issue if evidence is missing, otherwise null
|
|
285
|
+
*/
|
|
286
|
+
function detectMissingEvidence(ac) {
|
|
287
|
+
if (ac.evidence)
|
|
288
|
+
return null;
|
|
289
|
+
if (ac.verificationMethod === "manual")
|
|
290
|
+
return null;
|
|
291
|
+
return {
|
|
292
|
+
type: "incomplete",
|
|
293
|
+
matchedPattern: ac.verificationMethod,
|
|
294
|
+
problem: `Incomplete: verification not named — method "${ac.verificationMethod}" was inferred from keywords, not declared`,
|
|
295
|
+
suggestion: "Add a trailing `Evidence:` clause naming the command or check that verifies this AC (e.g., `Evidence: \\`npm test -- foo\\``).",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
275
298
|
/**
|
|
276
299
|
* Lint a single acceptance criterion against all patterns
|
|
277
300
|
*
|
|
@@ -296,6 +319,9 @@ export function lintAcceptanceCriterion(ac, patterns = DEFAULT_LINT_PATTERNS) {
|
|
|
296
319
|
const tension = detectTitleBodyTension(ac);
|
|
297
320
|
if (tension)
|
|
298
321
|
issues.push(tension);
|
|
322
|
+
const missingEvidence = detectMissingEvidence(ac);
|
|
323
|
+
if (missingEvidence)
|
|
324
|
+
issues.push(missingEvidence);
|
|
299
325
|
return {
|
|
300
326
|
ac,
|
|
301
327
|
issues,
|