sequant 2.11.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +47 -3
  4. package/dist/dashboard/server.js +4 -0
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +9 -0
  8. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
  9. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  10. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  11. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  12. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  13. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  14. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  15. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  16. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +42 -20
  18. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  19. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  20. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  21. package/dist/src/commands/doctor.js +20 -18
  22. package/dist/src/commands/ready.js +7 -1
  23. package/dist/src/commands/status.js +4 -0
  24. package/dist/src/lib/ac-linter.js +26 -0
  25. package/dist/src/lib/ac-parser.d.ts +40 -0
  26. package/dist/src/lib/ac-parser.js +202 -16
  27. package/dist/src/lib/markdown-fence.d.ts +24 -0
  28. package/dist/src/lib/markdown-fence.js +51 -0
  29. package/dist/src/lib/mcp-config.d.ts +24 -0
  30. package/dist/src/lib/mcp-config.js +51 -0
  31. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  32. package/dist/src/lib/scope/analyzer.js +7 -1
  33. package/dist/src/lib/settings.d.ts +73 -14
  34. package/dist/src/lib/settings.js +45 -3
  35. package/dist/src/lib/system.d.ts +7 -3
  36. package/dist/src/lib/system.js +7 -3
  37. package/dist/src/lib/test-tautology-detector.js +50 -3
  38. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  39. package/dist/src/lib/workflow/batch-executor.js +81 -6
  40. package/dist/src/lib/workflow/config-resolver.d.ts +30 -2
  41. package/dist/src/lib/workflow/config-resolver.js +59 -2
  42. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +14 -0
  43. package/dist/src/lib/workflow/drivers/claude-code.js +36 -4
  44. package/dist/src/lib/workflow/metrics-schema.d.ts +10 -1
  45. package/dist/src/lib/workflow/metrics-schema.js +13 -1
  46. package/dist/src/lib/workflow/metrics-writer.d.ts +3 -1
  47. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  48. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  49. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  50. package/dist/src/lib/workflow/phase-executor.js +60 -6
  51. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  52. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  53. package/dist/src/lib/workflow/ready-gate.d.ts +25 -1
  54. package/dist/src/lib/workflow/ready-gate.js +81 -11
  55. package/dist/src/lib/workflow/reconcile.js +4 -2
  56. package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
  57. package/dist/src/lib/workflow/run-log-schema.js +40 -0
  58. package/dist/src/lib/workflow/run-orchestrator.d.ts +18 -0
  59. package/dist/src/lib/workflow/run-orchestrator.js +38 -2
  60. package/dist/src/lib/workflow/state-cleanup.d.ts +4 -4
  61. package/dist/src/lib/workflow/state-cleanup.js +9 -5
  62. package/dist/src/lib/workflow/state-schema.d.ts +10 -1
  63. package/dist/src/lib/workflow/state-schema.js +13 -1
  64. package/dist/src/lib/workflow/types.d.ts +20 -0
  65. package/dist/src/mcp/tools/run.js +10 -1
  66. package/package.json +13 -12
  67. package/templates/hooks/pre-tool.sh +108 -17
  68. package/templates/memory/constitution.md +112 -45
  69. package/templates/skills/exec/SKILL.md +1 -1
  70. package/templates/skills/fullsolve/SKILL.md +62 -9
  71. package/templates/skills/loop/SKILL.md +71 -12
  72. package/templates/skills/merger/SKILL.md +32 -3
  73. package/templates/skills/qa/SKILL.md +247 -2
  74. package/templates/skills/spec/SKILL.md +11 -5
  75. package/templates/skills/test/SKILL.md +1 -1
@@ -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.10.0",
11
+ "version": "2.13.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.11.0",
4
+ "version": "2.13.0",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Sequant
2
2
 
3
- **Spec-driven AI coding agents — every acceptance criterion verified, stops at the human merge gate.**
3
+ **AI coding agents that prove their work — every acceptance criterion verified, and you hold the merge button.**
4
4
 
5
- For teams that can't ship un-reviewed AI code. Solve GitHub issues with structured phases and quality gates from issue to merge-ready PR.
5
+ For developers who won't merge what they can't trust solo or on a team. Solve GitHub issues with structured phases and quality gates, from issue to merge-ready PR.
6
6
 
7
7
  **[sequant.io](https://sequant.io)** — docs, guides, and getting started.
8
8
 
@@ -12,10 +12,30 @@ For teams that can't ship un-reviewed AI code. Solve GitHub issues with structur
12
12
  [![CI](https://github.com/sequant-io/sequant/actions/workflows/ci.yml/badge.svg)](https://github.com/sequant-io/sequant/actions/workflows/ci.yml)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- AI coding agents write code well, but leave you to run the workflow around it — planning, isolation, review, and merge safety. Sequant wraps an agent in a structured **spec → exec → qa** pipeline with isolated git worktrees and quality gates, taking a GitHub issue from triage to a merge-ready PR without babysitting each step.
15
+ AI coding agents write code well, but leave you to run the workflow around it — planning, isolation, review, and merge safety. Sequant wraps an agent in a structured **spec → exec → qa** pipeline with isolated git worktrees and quality gates, taking a GitHub issue from triage to a merge-ready PR without babysitting each step. The PR arrives with evidence — each acceptance criterion checked against the code — and the merge is always yours.
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.13
20
+
21
+ - **NEEDS_VERIFICATION verdicts stop blocking their own follow-up** — a QA verdict of NEEDS_VERIFICATION now maps to a dedicated `awaiting_verification` state instead of `ready_for_merge`, so after you execute the ACs the qa re-run just runs — no `--force`, no editing `state.json` by hand. The state shows up in `sequant status` and the dashboard with a re-run hint, and an issue whose PR you merge directly still sweeps to `merged`. MCP `sequant_run` also gains a real `force` parameter (previously silently ignored). The full verdict→state contract is documented in [qa-verdict-workflow-states.md](docs/features/qa-verdict-workflow-states.md) (#972).
22
+ - **A bad model name is now a loud failure, not a silent no-op** — when a phase's session ends on an API error (e.g. a typo'd or roster-stale model string), the driver now fails the phase with the API error text and structured `terminal_reason`/`api_error_status` context, instead of reporting a zero-work "success" that only surfaced downstream as an empty diff entering QA (#973).
23
+ - **Model roles: name the tier, not the model** — `run.modelRoles` maps semantic roles (defaults: `fast`, `strong`, `frontier`) to model strings, and phase policy or `--models` can reference them as `role:fast`. Raw model strings still pass through verbatim, a missing role fails loudly at config-resolution before any session spawns, and run metrics record both the requested value and the concrete model ID actually dispatched — so a roster change means editing one map, not every settings file (#975). `agents.model` now accepts any model string instead of a stale three-model enum.
24
+ - **A constitution with teeth** — the constitution template is now the enforceable agent contract: its Definition of Done table is *generated* from `/qa`'s real gate list and drift-gated in CI (`lint:constitution-dod`), an AC authoring standard is cross-linked from `/spec`'s lint output, and every Boundaries/Budgets rule names its actual enforcing mechanism — a gate test asserts every cited settings key and hook path resolves. Customized constitutions are preserved by `update`/`sync`; the new template reaches fresh `init` projects only (#943).
25
+
26
+ ### What's new in 2.12
27
+
28
+ - **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).
29
+ - **`/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).
30
+ - **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).
31
+
32
+ ### What's new in 2.11
33
+
34
+ - **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).
35
+ - **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).
36
+ - **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).
37
+ - **`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).
38
+
19
39
  ### What's new in 2.10
20
40
 
21
41
  - **`--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).
@@ -162,6 +182,17 @@ your-project/ # Main repo (stays on main branch)
162
182
  - Each issue has its own dependencies and build
163
183
  - Safe to discard failed experiments
164
184
 
185
+ ### Agent Contract
186
+
187
+ Every project gets `.claude/memory/constitution.md` — the machine-enforceable agent contract that answers "what will the agent refuse to do, what must every PR satisfy, and where does it stop for me?":
188
+
189
+ - **Definition of Done** — generated from `/qa`'s §7 gate list; a CI check (`lint:constitution-dod`) fails when the table drifts from the live gates. These are project-wide — issue ACs don't restate them.
190
+ - **AC Authoring Standard** — the house format for writing ACs (single-line, `Evidence:`/`Risk:`/`Human decision` fields, Non-Goals); referenced by `/spec`'s lint warnings.
191
+ - **Boundaries** — every rule names its enforcer: the force-push hook, the worktree-only editing guard, the mutation-verification gate.
192
+ - **Budgets & Stop Conditions** — iteration caps, the human merge gate, hold states, and gap-prompt discipline.
193
+
194
+ The file is a `CUSTOMIZABLE_FILES` entry: plain `sequant update` and `sync` preserve your edits. Only `--force` replaces it.
195
+
165
196
  ### Quality Gates
166
197
 
167
198
  Every `/qa` runs automated checks:
@@ -177,6 +208,19 @@ Every `/qa` runs automated checks:
177
208
 
178
209
  When checks fail, `/loop` automatically fixes and re-runs (up to 3x).
179
210
 
211
+ ### Thinking of building this on a graph framework?
212
+
213
+ You could assemble this workflow yourself on LangGraph, CrewAI, Mastra, or the Claude Agent SDK — they sell the primitives and leave the workflow as an exercise. Sequant is the finished, hardened version of what you'd end up building, in the frameworks' own vocabulary:
214
+
215
+ - **Isolated execution** — one git worktree per issue, resolved by the branch git reports rather than directory globs
216
+ - **Human-in-the-loop approval gate** — the pipeline stops at the PR and never merges; this is an invariant, not optional wiring
217
+ - **Guardrails** — QA verdicts with an enforced floor, mutation-verified gate tests, scope and security checks
218
+ - **Durable execution** — a rate-limit halt writes a resumable record; `sequant resume` picks up where it left off, skipping completed phases
219
+ - **State management** — per-issue and checkout-scoped locks, so concurrent sessions can't interleave git operations in the same tree
220
+ - **Deterministic control flow** — dependency-ordered batch scheduling over `blocked by #N` relationships
221
+
222
+ On a general framework, every one of these is something you wire up — and can wire wrong or skip. Here they hold for every run. The boring 80% (retries, resume, locking, exit codes your scripts can trust) has already been run in anger; the [CHANGELOG](CHANGELOG.md) is the receipts.
223
+
180
224
  ---
181
225
 
182
226
  ## Using Sequant
@@ -42,6 +42,8 @@ function getStatusClass(status) {
42
42
  return "primary";
43
43
  case "waiting_for_qa_gate":
44
44
  return "warning";
45
+ case "awaiting_verification":
46
+ return "warning";
45
47
  case "ready_for_merge":
46
48
  return "success";
47
49
  case "blocked":
@@ -347,6 +349,7 @@ function renderIssuesList(issues) {
347
349
  in_progress: [],
348
350
  waiting_for_qa_gate: [],
349
351
  waiting_for_human_merge: [],
352
+ awaiting_verification: [],
350
353
  ready_for_merge: [],
351
354
  blocked: [],
352
355
  not_started: [],
@@ -364,6 +367,7 @@ function renderIssuesList(issues) {
364
367
  const statusOrder = [
365
368
  "in_progress",
366
369
  "waiting_for_qa_gate",
370
+ "awaiting_verification",
367
371
  "ready_for_merge",
368
372
  "blocked",
369
373
  "not_started",
@@ -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.10.0",
4
+ "version": "2.13.0",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "sequant": {
3
3
  "command": "npx",
4
- "args": ["-y", "sequant@2.10.0", "serve"]
4
+ "args": ["-y", "sequant@2.13.0", "serve"]
5
5
  }
6
6
  }
@@ -75,6 +75,15 @@ sequant init
75
75
  - **Pre-tool guardrails** — blocks dangerous commands, enforces worktree safety
76
76
  - **Post-tool tracking** — timing, quality metrics, smart test runner
77
77
 
78
+ ### Agent Contract
79
+
80
+ Every project gets `.claude/memory/constitution.md` — the machine-enforceable agent contract that answers "what must every PR satisfy, and where does the agent stop?":
81
+
82
+ - **Definition of Done** — generated from `/qa`'s §7 gate list; a CI check fails on drift
83
+ - **AC Authoring Standard** — the house format for ACs; referenced by `/spec`'s lint warnings
84
+ - **Boundaries** — every rule names its enforcer (hook, gate, or settings key)
85
+ - **Budgets & Stop Conditions** — iteration caps, the human merge gate, and hold states
86
+
78
87
  ## Quick Start
79
88
 
80
89
  ```
@@ -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
- TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
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
- # Extract target directory from cd command if present (for worktree commits)
558
- # Handles: "cd /path && git commit" or "cd /path; git commit"
559
- TARGET_DIR=""
560
- if echo "$TOOL_INPUT" | grep -qE '^cd [^;&|]+'; then
561
- TARGET_DIR=$(echo "$TOOL_INPUT" | grep -oE '^cd [^;&|]+' | head -1 | sed 's/^cd //' | tr -d ' ')
562
- fi
563
-
564
- # Check for changes in the target directory (or current if no cd)
565
- if [[ -n "$TARGET_DIR" && -d "$TARGET_DIR" ]]; then
566
- CHANGES=$(cd "$TARGET_DIR" && git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
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
@@ -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 **4 custom agents** in `.claude/agents/`. These centralize model, permissions, effort, and tool restrictions that were previously duplicated inline.
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="sequant-explorer",
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 | `sequant-explorer` | Read-only, focused tools (declared model: haiku, inert per #43869) |
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 `sequant-explorer` or `Explore`
243
- - ~~`schema-inspector`~~ → Use `sequant-explorer` or `Explore`
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
- git worktree list | grep <N> || true
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