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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "sequant",
|
|
10
10
|
"description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Includes 17 skills, workflow MCP tools, and pre/post-tool hooks.",
|
|
11
|
-
"version": "2.
|
|
11
|
+
"version": "2.12.0",
|
|
12
12
|
"author": {
|
|
13
13
|
"name": "sequant-io",
|
|
14
14
|
"email": "hello@sequant.io"
|
|
@@ -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.12.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sequant-io",
|
|
7
7
|
"email": "hello@sequant.io"
|
package/README.md
CHANGED
|
@@ -16,6 +16,19 @@ AI coding agents write code well, but leave you to run the workflow around it
|
|
|
16
16
|
|
|
17
17
|
See the [CHANGELOG](CHANGELOG.md) for release notes, or the [migration guide](CHANGELOG.md#migration-from-v1x) if upgrading from v1.x.
|
|
18
18
|
|
|
19
|
+
### What's new in 2.12
|
|
20
|
+
|
|
21
|
+
- **QA gates you can parse, not just read** — `/qa` now closes every review with a structured findings marker (six-category taxonomy, evidence, recommended action) that `/loop` and `sequant ready` consume directly, so a finding QA itself called non-blocking is never burned as a fix iteration (#937). Gate-test ACs must ship a machine-checkable `SEQUANT_MUTATION` record in the PR body, enforced by `/qa` (#939), and an AC can declare its own verification command via a trailing `Evidence:` clause that QA must actually execute (#938). A new advisory CI job annotates PRs with likely-vacuous tests (#940).
|
|
22
|
+
- **`/fullsolve` stops at the PR** — it no longer merges the PR it creates; pass `--auto-merge` (or set `run.autoMerge: true`) for the previous end-to-end behavior (#958). `/merger` gains a Named-Set Boundary: it merges only the issues you name, halting with a report if a dependency outside that set turns up (#961).
|
|
23
|
+
- **Phase agents get an MCP allowlist, not your desktop config** — headless phase agents now receive the sequant MCP server plus the project's own `.mcp.json`, never a passthrough of Claude Desktop config (which can carry literal secrets into process argv). `run.mcpAllowlist` opts specific desktop-only servers back in deliberately (#936). See [run-command.md](docs/reference/run-command.md#allowlisting-a-desktop-only-server).
|
|
24
|
+
|
|
25
|
+
### What's new in 2.11
|
|
26
|
+
|
|
27
|
+
- **Per-phase `model`/`effort` configuration** — the new `sequant run`/`sequant ready` flags `--models`/`--efforts` (bare value applies to every phase, or a comma list of `phase=model` pairs) let a phase's Agent SDK session use a different Claude model or reasoning effort than the CLI default — e.g. planning with a stronger model and delegating implementation to a cheaper one. Absent by default: nothing changes unless configured. See [run-command.md](docs/reference/run-command.md#per-phase-model--effort).
|
|
28
|
+
- **Evidence-based effort escalation on retries** — `--escalate-effort` raises a retried phase execution's reasoning effort one tier above its resolved base, built on the per-phase effort resolver above. Escalates only on *observed* retry (an outer quality-loop iteration ≥ 2, or a `sequant ready` QA-pass ≥ 2) — never speculatively — so it can only trade cost for quality, never the reverse. See [run-command.md](docs/reference/run-command.md#effort-escalation-on-retries).
|
|
29
|
+
- **Checkout-scoped lock for the shared working tree** — the per-issue lock never protected the *checkout itself*: two sessions on different issues could still interleave `git checkout`/`reset`/`rebase`/`merge` in the same main working tree. `sequant locks checkout <acquire|release|check|clear>` and a `pre-tool.sh` hook now guard branch-mutating git in the main checkout directly, refusing a foreign session with the holder's identity and how to proceed (#901).
|
|
30
|
+
- **`sequant worktree resolve/verify`** — resolves and verifies an issue's worktree by the branch git reports rather than a directory-name glob, closing a shared-namespace collision that could point `/fullsolve`, `/exec`, `/qa`, `/loop`, `/testgen`, `/merger` or `/assess` at the wrong worktree (#899, #904).
|
|
31
|
+
|
|
19
32
|
### What's new in 2.10
|
|
20
33
|
|
|
21
34
|
- **`--auto-wait <minutes>` rides out a rate-limit window** — opt in and a run whose limit window is hours out sleeps until it reopens and continues, instead of halting for a manual restart (#804). **Off by default**; the value is a *total* budget per issue, capped at 2 waits. Never waits on out-of-credits failures (credits are purchased, not waited out). The wait is in-process — for waits that must survive closing the terminal or a reboot, see halt-and-resume below; an exhausted `--auto-wait` budget still writes the halt record so `sequant resume` can pick up where it gave up. See [run-command.md](docs/reference/run-command.md#auto-wait-for-a-rate-limit-window).
|
|
@@ -296,12 +309,16 @@ Multi-issue runs are parallel by default, and a per-issue lock (`.sequant/locks/
|
|
|
296
309
|
"run": {
|
|
297
310
|
"qualityLoop": false,
|
|
298
311
|
"maxIterations": 3,
|
|
299
|
-
"defaultBase": "feature/dashboard" // Optional: custom default base branch
|
|
312
|
+
"defaultBase": "feature/dashboard", // Optional: custom default base branch
|
|
313
|
+
"phases": {
|
|
314
|
+
"exec": { "model": "sonnet", "effort": "medium" } // Optional: per-phase model/effort override
|
|
315
|
+
},
|
|
316
|
+
"effortEscalation": false // Optional: escalate effort one tier on a quality-loop retry
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
```
|
|
303
320
|
|
|
304
|
-
See [Customization Guide](docs/guides/customization.md) for all options.
|
|
321
|
+
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
322
|
|
|
306
323
|
---
|
|
307
324
|
|
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,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.12.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sequant-io",
|
|
7
7
|
"email": "hello@sequant.io"
|
|
@@ -17,6 +17,16 @@ INPUT_JSON=$(cat)
|
|
|
17
17
|
# Parse JSON using jq (preferred) or fallback to grep
|
|
18
18
|
if command -v jq &>/dev/null; then
|
|
19
19
|
TOOL_NAME=$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')
|
|
20
|
+
# Claude Code's hook envelope carries the session id (same field
|
|
21
|
+
# capture-tokens.sh reads). Preferred holder identity for the checkout
|
|
22
|
+
# lock (#901): a skill shell's PID dies right after acquire, the session
|
|
23
|
+
# id does not. `// empty` keeps this safe if the field is ever absent —
|
|
24
|
+
# the guard then falls back to SEQUANT_ISSUE.
|
|
25
|
+
SESSION_ID=$(echo "$INPUT_JSON" | jq -r '.session_id // empty')
|
|
26
|
+
# The shell cwd the tool will run in. Distinct from CLAUDE_PROJECT_DIR,
|
|
27
|
+
# which stays pinned to the main checkout even while the agent works in a
|
|
28
|
+
# worktree — see the checkout-lock guard (#901).
|
|
29
|
+
HOOK_CWD=$(echo "$INPUT_JSON" | jq -r '.cwd // empty')
|
|
20
30
|
# For Bash tool, extract .command from tool_input; for others, stringify the whole object
|
|
21
31
|
if [[ "$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')" == "Bash" ]]; then
|
|
22
32
|
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input.command // empty')
|
|
@@ -25,9 +35,59 @@ if command -v jq &>/dev/null; then
|
|
|
25
35
|
fi
|
|
26
36
|
else
|
|
27
37
|
TOOL_NAME=$(echo "$INPUT_JSON" | grep -oE '"tool_name"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
38
|
+
SESSION_ID=$(echo "$INPUT_JSON" | grep -oE '"session_id"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
39
|
+
HOOK_CWD=$(echo "$INPUT_JSON" | grep -oE '"cwd"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
28
40
|
# For Bash tool, extract command from tool_input; for others, extract the whole object
|
|
29
41
|
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
30
|
-
|
|
42
|
+
# Escape-aware extraction (#963 gap B): the naive `grep -oE '"[^"]+"'`
|
|
43
|
+
# form (still used for the simpler fields above, where an embedded
|
|
44
|
+
# `\"` is implausible) stops at the FIRST escaped quote inside the
|
|
45
|
+
# JSON string, truncating any command containing one — e.g.
|
|
46
|
+
# `git commit -m "msg with \"quotes\""` would be cut down to just
|
|
47
|
+
# `git commit -m ` before it ever reaches the guards below.
|
|
48
|
+
#
|
|
49
|
+
# sed's `(([^"\\]|\\.)*)` captures the full escaped run — any run of
|
|
50
|
+
# non-quote/non-backslash chars, or a backslash paired with whatever
|
|
51
|
+
# follows it — up to the closing unescaped `"`. `JSON.stringify`
|
|
52
|
+
# guarantees the whole payload is one physical line, so a single
|
|
53
|
+
# sed pass is enough (no multi-line `-z` needed, which BSD sed lacks
|
|
54
|
+
# anyway).
|
|
55
|
+
#
|
|
56
|
+
# The captured text still carries JSON string escapes literally
|
|
57
|
+
# (`\"`, `\\`, `\n`, `\t`, ...); the awk pass resolves them in ONE
|
|
58
|
+
# left-to-right scan, consuming two characters per recognized
|
|
59
|
+
# escape. That ordering is what a chain of separate sed/tr
|
|
60
|
+
# substitutions cannot get right without a placeholder: a literal
|
|
61
|
+
# `\\n` in the original command is an escaped backslash (`\\`)
|
|
62
|
+
# immediately followed by a literal `n`, and must stay a backslash
|
|
63
|
+
# plus 'n' — not become an escaped-newline (`\n`) if the two escapes
|
|
64
|
+
# were resolved out of order. Scanning once and advancing past both
|
|
65
|
+
# characters of whichever escape is recognized sidesteps that
|
|
66
|
+
# ambiguity entirely.
|
|
67
|
+
TOOL_INPUT=$(printf '%s' "$INPUT_JSON" \
|
|
68
|
+
| sed -E -n 's/.*"command"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p' \
|
|
69
|
+
| head -1 \
|
|
70
|
+
| awk '
|
|
71
|
+
{
|
|
72
|
+
s = $0; out = ""; n = length(s)
|
|
73
|
+
for (i = 1; i <= n; i++) {
|
|
74
|
+
c = substr(s, i, 1)
|
|
75
|
+
if (c == "\\" && i < n) {
|
|
76
|
+
nc = substr(s, i + 1, 1)
|
|
77
|
+
if (nc == "\"") { out = out "\""; i++ }
|
|
78
|
+
else if (nc == "\\") { out = out "\\"; i++ }
|
|
79
|
+
else if (nc == "n") { out = out "\n"; i++ }
|
|
80
|
+
else if (nc == "t") { out = out "\t"; i++ }
|
|
81
|
+
else if (nc == "r") { out = out "\r"; i++ }
|
|
82
|
+
else if (nc == "/") { out = out "/"; i++ }
|
|
83
|
+
else { out = out c }
|
|
84
|
+
} else {
|
|
85
|
+
out = out c
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
print out
|
|
89
|
+
}
|
|
90
|
+
')
|
|
31
91
|
else
|
|
32
92
|
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_input"\s*:\s*\{[^}]+\}' | head -1)
|
|
33
93
|
fi
|
|
@@ -246,6 +306,51 @@ seg_match() {
|
|
|
246
306
|
[[ -n "$SEGMENTS" ]] && grep -qE "$1" <<< "$SEGMENTS"
|
|
247
307
|
}
|
|
248
308
|
|
|
309
|
+
# resolve_cd_target <tool_input> — print the target directory of the LAST
|
|
310
|
+
# `cd <path>` line in a (possibly multi-line) Bash command, if and only if
|
|
311
|
+
# the path is a static literal (quoted or unquoted) that resolves to an
|
|
312
|
+
# existing directory. Scans the raw command, not $SEGMENTS — emit_segments
|
|
313
|
+
# drops double-quoted regions, so `cd "$WT"` would vanish there before this
|
|
314
|
+
# ever saw it. Prints nothing when there is no `cd` line, the target is
|
|
315
|
+
# dynamic (contains `$` or a backtick), or the path doesn't exist — callers
|
|
316
|
+
# must treat empty output as "fail open", never as license to guess a
|
|
317
|
+
# directory (#963).
|
|
318
|
+
resolve_cd_target() {
|
|
319
|
+
local input="$1" line target
|
|
320
|
+
line=$(printf '%s\n' "$input" | grep -E '^[[:space:]]*cd[[:space:]]+' | tail -1)
|
|
321
|
+
[[ -z "$line" ]] && return 0
|
|
322
|
+
|
|
323
|
+
target=$(printf '%s' "$line" | sed -E 's/^[[:space:]]*cd[[:space:]]+//; s/[[:space:]]*[;&|].*$//; s/[[:space:]]+$//')
|
|
324
|
+
|
|
325
|
+
# Strip one layer of surrounding matching quotes.
|
|
326
|
+
case "$target" in
|
|
327
|
+
\"*\") target="${target#\"}"; target="${target%\"}" ;;
|
|
328
|
+
\'*\') target="${target#\'}"; target="${target%\'}" ;;
|
|
329
|
+
esac
|
|
330
|
+
|
|
331
|
+
# Fail open on anything dynamic — resolving shell expansions means
|
|
332
|
+
# reimplementing the shell, which is disproportionate; `git commit`
|
|
333
|
+
# itself already rejects a genuinely empty commit. A backslash is
|
|
334
|
+
# rejected too: it can escape a following `$`/`` ` `` into a form this
|
|
335
|
+
# literal-string check would otherwise miss, and a backslash also carries
|
|
336
|
+
# its own shell meaning (line continuation, escaped chars) that this
|
|
337
|
+
# function does not attempt to resolve — failing open is the safe
|
|
338
|
+
# direction either way (#963).
|
|
339
|
+
case "$target" in
|
|
340
|
+
*'$'*|*'`'*|*'\'*) return 0 ;;
|
|
341
|
+
esac
|
|
342
|
+
|
|
343
|
+
[[ -n "$target" && -d "$target" ]] && printf '%s' "$target"
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Path of the session->issue binding the checkout guard maintains (#906).
|
|
347
|
+
# $1 = repo toplevel, $2 = session id. The id is opaque, so squash everything
|
|
348
|
+
# outside a filename-safe set — it must not be able to escape the directory.
|
|
349
|
+
_co_binding_path() {
|
|
350
|
+
printf '%s/.sequant/locks/session-%s.issue' \
|
|
351
|
+
"$1" "$(printf '%s' "$2" | tr -c 'A-Za-z0-9_-' '_')"
|
|
352
|
+
}
|
|
353
|
+
|
|
249
354
|
# Precompute the segment list once, for Bash commands only.
|
|
250
355
|
SEGMENTS=""
|
|
251
356
|
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
@@ -458,6 +563,213 @@ if seg_match 'git reset.*(--hard|origin)'; then
|
|
|
458
563
|
fi
|
|
459
564
|
fi
|
|
460
565
|
|
|
566
|
+
# --- Session -> issue binding for the checkout guard (Issue #906) ---
|
|
567
|
+
# `SEQUANT_ISSUE` cannot identify the holder interactively, and never could:
|
|
568
|
+
# PreToolUse runs OUTSIDE and BEFORE the command's shell, so nothing a skill
|
|
569
|
+
# bash block exports is visible here — not even an export prepended to the same
|
|
570
|
+
# block as the guarded command. The one path that does export it (`sequant run`)
|
|
571
|
+
# also sets SEQUANT_ORCHESTRATOR, where this guard stands down. So the env
|
|
572
|
+
# fallback below was unreachable in every real flow, and the holder was
|
|
573
|
+
# routinely blocked by its own lock.
|
|
574
|
+
#
|
|
575
|
+
# The hook does see both the acquire and every later command of the same
|
|
576
|
+
# session, and `session_id` survives the shell boundary that kills the
|
|
577
|
+
# acquiring PID. Record the binding when we observe the acquire; read it back
|
|
578
|
+
# when deciding whether the caller is the holder.
|
|
579
|
+
if [[ -n "${SESSION_ID:-}" ]] && seg_match 'locks +checkout +(acquire|release)'; then
|
|
580
|
+
_CO_SB_ROOT=$(git -C "${HOOK_CWD:-$PWD}" rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
581
|
+
if [[ -n "$_CO_SB_ROOT" && -d "$_CO_SB_ROOT/.git" ]]; then
|
|
582
|
+
_CO_SB_FILE=$(_co_binding_path "$_CO_SB_ROOT" "$SESSION_ID")
|
|
583
|
+
_CO_SB_ISSUE=$(printf '%s' "$TOOL_INPUT" \
|
|
584
|
+
| grep -oE '\-\-issue[= ]+[0-9]+' | head -1 | grep -oE '[0-9]+$' || true)
|
|
585
|
+
if seg_match 'locks +checkout +acquire'; then
|
|
586
|
+
if [[ -n "$_CO_SB_ISSUE" ]]; then
|
|
587
|
+
mkdir -p "$(dirname "$_CO_SB_FILE")" 2>/dev/null \
|
|
588
|
+
&& printf '%s' "$_CO_SB_ISSUE" > "$_CO_SB_FILE" 2>/dev/null || true
|
|
589
|
+
fi
|
|
590
|
+
elif [[ -f "$_CO_SB_FILE" ]]; then
|
|
591
|
+
# Clear only when the session releases its OWN claim. A refused
|
|
592
|
+
# release (wrong --issue) must not strip the real holder's identity
|
|
593
|
+
# and leave it blocked by its own lock.
|
|
594
|
+
if [[ -n "$_CO_SB_ISSUE" \
|
|
595
|
+
&& "$_CO_SB_ISSUE" == "$(cat "$_CO_SB_FILE" 2>/dev/null)" ]]; then
|
|
596
|
+
rm -f "$_CO_SB_FILE" 2>/dev/null || true
|
|
597
|
+
fi
|
|
598
|
+
fi
|
|
599
|
+
fi
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
# --- Checkout-scoped lock enforcement (Issue #901) ---
|
|
603
|
+
# The per-issue lock (#625) keys on issue number, so two sessions working
|
|
604
|
+
# *different* issues take different lock files and never contend. But
|
|
605
|
+
# `git checkout`, `switch`, `reset`, `rebase`, `merge` and `cherry-pick` are
|
|
606
|
+
# global to a working tree — the contended resource is the checkout, not the
|
|
607
|
+
# issue. `.sequant/locks/checkout.lock` represents the tree; this guard is what
|
|
608
|
+
# makes it binding, because the racing actor is an agent's Bash command, not
|
|
609
|
+
# sequant's TypeScript (which mutates git almost exclusively via `git -C
|
|
610
|
+
# <worktree>`).
|
|
611
|
+
#
|
|
612
|
+
# STALENESS IS A DELIBERATELY WEAKER SUBSET, NOT A MIRROR. The authoritative
|
|
613
|
+
# rules live in `classifyStaleness` (src/lib/locks/lock-manager.ts) and are
|
|
614
|
+
# shared by CheckoutLock. Transcribing them into shell would drift (#871), so
|
|
615
|
+
# this guard checks only the absolute age ceiling and FAILS OPEN past it. A
|
|
616
|
+
# lock this guard lets through is still caught by the TypeScript path; a lock
|
|
617
|
+
# it blocks on is always genuinely fresh. Weaker-but-honest beats a mirror.
|
|
618
|
+
#
|
|
619
|
+
# AC-5: orchestrator/MCP mode is a no-op here too, matching LockManager and
|
|
620
|
+
# CheckoutLock — `sequant run` drives its own worktree isolation and must not
|
|
621
|
+
# be blocked by a lock its own skills took.
|
|
622
|
+
if [[ -z "${SEQUANT_ORCHESTRATOR:-}" ]] \
|
|
623
|
+
&& seg_match 'git (checkout|switch|reset|rebase|merge|cherry-pick)( |$)' \
|
|
624
|
+
&& ! seg_match 'git +-C ' \
|
|
625
|
+
&& ! seg_match 'git checkout ([^ ]+ )?--( |$)'; then
|
|
626
|
+
|
|
627
|
+
# Only the MAIN checkout is protected — a command run inside a worktree
|
|
628
|
+
# touches only that worktree's HEAD and must never be blocked.
|
|
629
|
+
#
|
|
630
|
+
# Resolve where the command will ACTUALLY run. This must NOT use
|
|
631
|
+
# CLAUDE_PROJECT_DIR / PARALLEL_MARKER_PROJECT_ROOT: those name the
|
|
632
|
+
# *project* directory, which stays pinned to the main checkout even while
|
|
633
|
+
# the agent's shell sits in a worktree. Keying off them blocked legitimate
|
|
634
|
+
# in-worktree work — the guard's worst failure mode, since the whole point
|
|
635
|
+
# of the lock is to push sessions *into* worktrees.
|
|
636
|
+
#
|
|
637
|
+
# `.cwd` is part of Claude Code's PreToolUse envelope (verified against a
|
|
638
|
+
# live payload alongside `session_id`), with $PWD as the fallback.
|
|
639
|
+
_CO_CWD="${HOOK_CWD:-$PWD}"
|
|
640
|
+
# Honor a `cd <dir>` the same way the commit guard below does — including
|
|
641
|
+
# multi-line commands and quoted/dynamic targets (#963).
|
|
642
|
+
_CO_CD=$(resolve_cd_target "$TOOL_INPUT")
|
|
643
|
+
[[ -n "$_CO_CD" ]] && _CO_CWD="$_CO_CD"
|
|
644
|
+
|
|
645
|
+
# A linked worktree's toplevel has `.git` as a FILE; the main checkout has
|
|
646
|
+
# it as a directory.
|
|
647
|
+
_CO_ROOT=$(git -C "$_CO_CWD" rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
648
|
+
if [[ -n "$_CO_ROOT" && -d "$_CO_ROOT/.git" ]]; then
|
|
649
|
+
_CO_LOCK="$_CO_ROOT/.sequant/locks/checkout.lock"
|
|
650
|
+
|
|
651
|
+
if [[ -f "$_CO_LOCK" ]]; then
|
|
652
|
+
if command -v jq &>/dev/null; then
|
|
653
|
+
_CO_HOLDER_ISSUE=$(jq -r '.issue // empty' "$_CO_LOCK" 2>/dev/null)
|
|
654
|
+
_CO_HOLDER_SESSION=$(jq -r '.sessionId // empty' "$_CO_LOCK" 2>/dev/null)
|
|
655
|
+
_CO_HOLDER_PID=$(jq -r '.pid // empty' "$_CO_LOCK" 2>/dev/null)
|
|
656
|
+
_CO_HOLDER_HOST=$(jq -r '.hostname // empty' "$_CO_LOCK" 2>/dev/null)
|
|
657
|
+
_CO_HOLDER_STARTED=$(jq -r '.startedAt // empty' "$_CO_LOCK" 2>/dev/null)
|
|
658
|
+
_CO_HOLDER_CMD=$(jq -r '.command // empty' "$_CO_LOCK" 2>/dev/null)
|
|
659
|
+
else
|
|
660
|
+
_CO_HOLDER_ISSUE=$(grep -oE '"issue"[[:space:]]*:[[:space:]]*[0-9]+' "$_CO_LOCK" | head -1 | grep -oE '[0-9]+$')
|
|
661
|
+
_CO_HOLDER_SESSION=$(grep -oE '"sessionId"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
662
|
+
_CO_HOLDER_PID=$(grep -oE '"pid"[[:space:]]*:[[:space:]]*[0-9]+' "$_CO_LOCK" | head -1 | grep -oE '[0-9]+$')
|
|
663
|
+
_CO_HOLDER_HOST=$(grep -oE '"hostname"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
664
|
+
_CO_HOLDER_STARTED=$(grep -oE '"startedAt"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
665
|
+
_CO_HOLDER_CMD=$(grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
# Staleness. These branches mirror `classifyStaleness`
|
|
669
|
+
# (src/lib/locks/lock-manager.ts) in the same order, because AC-4
|
|
670
|
+
# requires the checkout lock's stale recovery to match the per-issue
|
|
671
|
+
# lock's — same-host dead PID, age ceiling, and the env overrides.
|
|
672
|
+
# Implementing only a subset here would let a *dead* holder block
|
|
673
|
+
# the tree for up to 24h, which is the wedge AC-4 forbids.
|
|
674
|
+
#
|
|
675
|
+
# The three rules are plain comparisons plus one `kill -0`, so this
|
|
676
|
+
# is a small enough surface to keep honest; the "hook/TypeScript
|
|
677
|
+
# staleness parity" cases in checkout-lock.integration.test.ts pin
|
|
678
|
+
# both sides to the same verdict so they cannot drift silently
|
|
679
|
+
# (#871 — the repo's drift guard compares literal strings only and
|
|
680
|
+
# would not see a semantic divergence here).
|
|
681
|
+
_CO_MAX_AGE_MS="${SEQUANT_MAX_LOCK_AGE_MS:-86400000}" # 24h ceiling
|
|
682
|
+
_CO_SKILL_TTL_MS="${SEQUANT_SKILL_LOCK_TTL_MS:-21600000}" # 6h skill-shell
|
|
683
|
+
_CO_STALE_AGE_MS=7200000 # 2h cross-host
|
|
684
|
+
_CO_FRESH=true
|
|
685
|
+
|
|
686
|
+
# `startedAt` is ISO-8601 **UTC**. BSD `date -j -f` parses in LOCAL
|
|
687
|
+
# time, so without TZ=UTC the age comes out shifted by the UTC
|
|
688
|
+
# offset — west of UTC that is *negative*, and a stale lock then
|
|
689
|
+
# reads as fresh forever, wedging the tree. TZ=UTC pins the BSD
|
|
690
|
+
# branch; Linux/CI falls through to GNU `date -u -d`, which honors
|
|
691
|
+
# the trailing Z.
|
|
692
|
+
_CO_AGE_MS=""
|
|
693
|
+
if [[ -n "$_CO_HOLDER_STARTED" ]]; then
|
|
694
|
+
_CO_STARTED_EPOCH=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "${_CO_HOLDER_STARTED%%.*}" +%s 2>/dev/null \
|
|
695
|
+
|| date -u -d "$_CO_HOLDER_STARTED" +%s 2>/dev/null || echo "")
|
|
696
|
+
if [[ -n "$_CO_STARTED_EPOCH" ]]; then
|
|
697
|
+
_CO_AGE_MS=$(( ( $(date +%s) - _CO_STARTED_EPOCH ) * 1000 ))
|
|
698
|
+
# Negative age = clock skew between hosts. Treat as unknown
|
|
699
|
+
# rather than stale: refusing is recoverable, silently
|
|
700
|
+
# ignoring a live holder is not.
|
|
701
|
+
[[ "$_CO_AGE_MS" -lt 0 ]] && _CO_AGE_MS=""
|
|
702
|
+
fi
|
|
703
|
+
fi
|
|
704
|
+
|
|
705
|
+
_CO_SKIP_PID=false
|
|
706
|
+
grep -q '"skipPidCheck"[[:space:]]*:[[:space:]]*true' "$_CO_LOCK" 2>/dev/null && _CO_SKIP_PID=true
|
|
707
|
+
|
|
708
|
+
# 0. Absolute ceiling, checked first and unconditionally (#856):
|
|
709
|
+
# past it a PID is no longer trustworthy identity.
|
|
710
|
+
if [[ -n "$_CO_AGE_MS" && "$_CO_AGE_MS" -gt "$_CO_MAX_AGE_MS" ]]; then
|
|
711
|
+
_CO_FRESH=false
|
|
712
|
+
# 1. Same-host PID check is authoritative — unless the holder asked
|
|
713
|
+
# us to skip it (a skill shell whose PID dies after acquire).
|
|
714
|
+
elif [[ "$_CO_HOLDER_HOST" == "$(hostname)" && "$_CO_SKIP_PID" == "false" ]]; then
|
|
715
|
+
# `kill -0` is a bash builtin: no subprocess on the hot path.
|
|
716
|
+
if [[ -n "$_CO_HOLDER_PID" ]] && ! kill -0 "$_CO_HOLDER_PID" 2>/dev/null; then
|
|
717
|
+
_CO_FRESH=false
|
|
718
|
+
fi
|
|
719
|
+
# 2. Cross-host or skipPidCheck: the PID is meaningless, use age.
|
|
720
|
+
elif [[ -n "$_CO_AGE_MS" ]]; then
|
|
721
|
+
if [[ "$_CO_SKIP_PID" == "true" ]]; then
|
|
722
|
+
_CO_TTL_MS="$_CO_SKILL_TTL_MS"
|
|
723
|
+
else
|
|
724
|
+
_CO_TTL_MS="$_CO_STALE_AGE_MS"
|
|
725
|
+
fi
|
|
726
|
+
[[ "$_CO_AGE_MS" -gt "$_CO_TTL_MS" ]] && _CO_FRESH=false
|
|
727
|
+
fi
|
|
728
|
+
|
|
729
|
+
# Is this session the holder? sessionId is the only identity that
|
|
730
|
+
# survives a skill shell exiting between acquire and this call, so
|
|
731
|
+
# it wins when both sides have one. Otherwise fall back to the
|
|
732
|
+
# issue this session is working on.
|
|
733
|
+
_CO_IS_HOLDER=false
|
|
734
|
+
if [[ -n "$_CO_HOLDER_SESSION" && -n "$SESSION_ID" ]]; then
|
|
735
|
+
[[ "$_CO_HOLDER_SESSION" == "$SESSION_ID" ]] && _CO_IS_HOLDER=true
|
|
736
|
+
elif [[ -n "${SEQUANT_ISSUE:-}" && -n "$_CO_HOLDER_ISSUE" ]]; then
|
|
737
|
+
# Reachable only from a parent process that exported it — never
|
|
738
|
+
# from a skill bash block (#906). Kept for `sequant run`-shaped
|
|
739
|
+
# callers; the binding below is what works interactively.
|
|
740
|
+
[[ "${SEQUANT_ISSUE}" == "$_CO_HOLDER_ISSUE" ]] && _CO_IS_HOLDER=true
|
|
741
|
+
elif [[ -n "${SESSION_ID:-}" && -n "$_CO_HOLDER_ISSUE" ]]; then
|
|
742
|
+
# The binding this hook recorded when it saw THIS session run
|
|
743
|
+
# `locks checkout acquire --issue=N` (#906).
|
|
744
|
+
_CO_BIND=$(_co_binding_path "$_CO_ROOT" "$SESSION_ID")
|
|
745
|
+
[[ -f "$_CO_BIND" \
|
|
746
|
+
&& "$(cat "$_CO_BIND" 2>/dev/null)" == "$_CO_HOLDER_ISSUE" ]] \
|
|
747
|
+
&& _CO_IS_HOLDER=true
|
|
748
|
+
fi
|
|
749
|
+
|
|
750
|
+
if [[ "$_CO_FRESH" == "true" && "$_CO_IS_HOLDER" == "false" ]]; then
|
|
751
|
+
log_block "checkout-lock"
|
|
752
|
+
{
|
|
753
|
+
echo "HOOK_BLOCKED: Checkout held by another session"
|
|
754
|
+
echo ""
|
|
755
|
+
echo " The working tree is held by the session working #${_CO_HOLDER_ISSUE:-?}"
|
|
756
|
+
echo " (PID ${_CO_HOLDER_PID:-?} on ${_CO_HOLDER_HOST:-?}, started ${_CO_HOLDER_STARTED:-?})."
|
|
757
|
+
echo " Command: ${_CO_HOLDER_CMD:-?}"
|
|
758
|
+
echo ""
|
|
759
|
+
echo " Branch-mutating git here would race with that session."
|
|
760
|
+
echo ""
|
|
761
|
+
echo " To proceed:"
|
|
762
|
+
echo " • Work in your own worktree: ../worktrees/feature/<your-issue>-*/"
|
|
763
|
+
echo " (create it with: ./scripts/new-feature.sh <your-issue>)"
|
|
764
|
+
echo " • Or target it explicitly: git -C <worktree> <command>"
|
|
765
|
+
echo " • If that session is gone: sequant locks checkout clear --force"
|
|
766
|
+
} >&2
|
|
767
|
+
exit 2
|
|
768
|
+
fi
|
|
769
|
+
fi
|
|
770
|
+
fi
|
|
771
|
+
fi
|
|
772
|
+
|
|
461
773
|
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
462
774
|
if seg_match 'gh workflow run'; then
|
|
463
775
|
log_block "workflow-trigger"
|
|
@@ -554,18 +866,25 @@ fi
|
|
|
554
866
|
# Skips for --amend since amending doesn't require new changes
|
|
555
867
|
if [[ "$TOOL_NAME" == "Bash" ]] && seg_match 'git commit'; then
|
|
556
868
|
if ! echo "$TOOL_INPUT" | grep -qE -- '--amend|--allow-empty'; then
|
|
557
|
-
#
|
|
558
|
-
#
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if [[ -n "$TARGET_DIR"
|
|
566
|
-
CHANGES=$(
|
|
869
|
+
# Resolve where to check for changes: the last resolvable `cd`
|
|
870
|
+
# target if the command has one (multi-line commands included —
|
|
871
|
+
# #963), else the command's own cwd from the hook payload (never
|
|
872
|
+
# this hook process's own cwd, which need not match).
|
|
873
|
+
TARGET_DIR=$(resolve_cd_target "$TOOL_INPUT")
|
|
874
|
+
HAS_CD_LINE=false
|
|
875
|
+
echo "$TOOL_INPUT" | grep -qE '^[[:space:]]*cd[[:space:]]+' && HAS_CD_LINE=true
|
|
876
|
+
|
|
877
|
+
if [[ -n "$TARGET_DIR" ]]; then
|
|
878
|
+
CHANGES=$(git -C "$TARGET_DIR" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
879
|
+
elif [[ "$HAS_CD_LINE" == true ]]; then
|
|
880
|
+
# A `cd` line is present but its target is dynamic (a shell
|
|
881
|
+
# variable/command substitution) or doesn't exist as a
|
|
882
|
+
# directory — fail open rather than check the wrong
|
|
883
|
+
# directory. `git commit` itself already rejects a genuinely
|
|
884
|
+
# empty commit, so this costs one harmless git error (#963).
|
|
885
|
+
CHANGES=1
|
|
567
886
|
else
|
|
568
|
-
CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
887
|
+
CHANGES=$(git -C "${HOOK_CWD:-$PWD}" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
569
888
|
fi
|
|
570
889
|
|
|
571
890
|
if [[ "$CHANGES" -eq 0 ]]; then
|
package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md
CHANGED
|
@@ -15,7 +15,7 @@ Claude Code supports exactly **4 built-in subagent types**:
|
|
|
15
15
|
|
|
16
16
|
## Custom Agents (Sequant)
|
|
17
17
|
|
|
18
|
-
Sequant defines **
|
|
18
|
+
Sequant defines **3 custom agents** in `.claude/agents/`. These centralize model, permissions, effort, and tool restrictions that were previously duplicated inline.
|
|
19
19
|
|
|
20
20
|
> **Upstream caveat:** `Model` values below are *declared* in the agent files but
|
|
21
21
|
> currently ignored at runtime per anthropics/claude-code#43869 — every subagent
|
|
@@ -24,20 +24,10 @@ Sequant defines **4 custom agents** in `.claude/agents/`. These centralize model
|
|
|
24
24
|
|
|
25
25
|
| Agent Name | Based On | Model (declared) | Permission Mode | Used By |
|
|
26
26
|
|------------|----------|------------------|-----------------|---------|
|
|
27
|
-
| `sequant-explorer` | Explore | haiku | (default) | `/spec` |
|
|
28
27
|
| `sequant-qa-checker` | general-purpose | sonnet | bypassPermissions | `/qa` |
|
|
29
28
|
| `sequant-implementer` | general-purpose | (inherits) | bypassPermissions | `/exec` |
|
|
30
29
|
| `sequant-testgen` | general-purpose | haiku | (default) | `/testgen` |
|
|
31
30
|
|
|
32
|
-
### sequant-explorer
|
|
33
|
-
|
|
34
|
-
Read-only codebase exploration for the `/spec` phase. No Bash, Edit, or Write access.
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
Agent(subagent_type="sequant-explorer",
|
|
38
|
-
prompt="Find similar features in components/. Report patterns.")
|
|
39
|
-
```
|
|
40
|
-
|
|
41
31
|
### sequant-qa-checker
|
|
42
32
|
|
|
43
33
|
Quality check agent for the `/qa` phase. Has `bypassPermissions` for Bash access (git diff, npm test). Effort: low.
|
|
@@ -175,12 +165,11 @@ Agent(subagent_type="sequant-qa-checker",
|
|
|
175
165
|
```
|
|
176
166
|
|
|
177
167
|
### Context Gathering (via /spec)
|
|
168
|
+
|
|
169
|
+
`/spec` defaults to targeted inline `Read`/`Grep`, not an agent spawn. It escalates to a single `Explore` agent only for open-ended discovery:
|
|
178
170
|
```
|
|
179
|
-
Agent(subagent_type="
|
|
171
|
+
Agent(subagent_type="Explore",
|
|
180
172
|
prompt="Find similar features in components/. Report patterns.")
|
|
181
|
-
|
|
182
|
-
Agent(subagent_type="sequant-explorer",
|
|
183
|
-
prompt="Explore database schema for user tables. Report structure.")
|
|
184
173
|
```
|
|
185
174
|
|
|
186
175
|
### Background Execution (via /exec)
|
|
@@ -215,7 +204,7 @@ inline when spawning them.
|
|
|
215
204
|
| Task | Recommended Agent | Why |
|
|
216
205
|
|------|-------------------|-----|
|
|
217
206
|
| Quality checks (git diff, npm test) | `sequant-qa-checker` | bypassPermissions + effort:low (declared model: sonnet, inert per #43869) |
|
|
218
|
-
| Codebase exploration | `
|
|
207
|
+
| Codebase exploration (open-ended only) | `Explore` | Read-only, built-in; `/spec` prefers inline Read/Grep by default |
|
|
219
208
|
| Implementation subtask | `sequant-implementer` | Full access, inherits model |
|
|
220
209
|
| Test stub generation | `sequant-testgen` | Write access, no Bash (declared model: haiku, inert per #43869) |
|
|
221
210
|
| One-off custom task | `general-purpose` | Flexible, specify model/mode inline |
|
|
@@ -239,8 +228,8 @@ inline when spawning them.
|
|
|
239
228
|
These types do **not exist** and will cause silent failures:
|
|
240
229
|
|
|
241
230
|
- ~~`quality-checker`~~ → Use `sequant-qa-checker` or `general-purpose`
|
|
242
|
-
- ~~`pattern-scout`~~ → Use `
|
|
243
|
-
- ~~`schema-inspector`~~ → Use `
|
|
231
|
+
- ~~`pattern-scout`~~ → Use `Explore`
|
|
232
|
+
- ~~`schema-inspector`~~ → Use `Explore`
|
|
244
233
|
- ~~`code-reviewer`~~ → Use `sequant-qa-checker` or `general-purpose`
|
|
245
234
|
- ~~`implementation`~~ → Use `sequant-implementer` or `general-purpose`
|
|
246
235
|
|
|
@@ -6,6 +6,7 @@ metadata:
|
|
|
6
6
|
author: sequant
|
|
7
7
|
version: "3.0"
|
|
8
8
|
allowed-tools:
|
|
9
|
+
- Bash(npx sequant worktree:*)
|
|
9
10
|
- Read
|
|
10
11
|
- Glob
|
|
11
12
|
- Grep
|
|
@@ -99,7 +100,10 @@ gh issue view <N> --json title,body,labels,state,comments,assignees
|
|
|
99
100
|
|
|
100
101
|
```bash
|
|
101
102
|
git branch -a | grep <N> || true
|
|
102
|
-
|
|
103
|
+
# Resolve by branch, not by grepping the printed path (#899/#904) — a bare
|
|
104
|
+
# number match is cross-issue (89 matches 899-...) and keys on the directory
|
|
105
|
+
# slug, which can diverge from the branch after a rename.
|
|
106
|
+
npx sequant worktree resolve <N> || true
|
|
103
107
|
gh pr list --search "<N> in:title" --json number,title,state,headRefName,mergeable || true
|
|
104
108
|
```
|
|
105
109
|
|