sequant 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/dist/bin/cli.js +47 -2
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +206 -4
- package/dist/src/commands/ready.d.ts +6 -0
- package/dist/src/commands/ready.js +15 -1
- package/dist/src/commands/run-display.js +1 -0
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/cli-flags.d.ts +23 -0
- package/dist/src/lib/cli-flags.js +43 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
- package/dist/src/lib/cli-ui/run-renderer.js +7 -1
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +6 -3
- package/dist/src/lib/locks/index.js +4 -2
- package/dist/src/lib/locks/lock-manager.d.ts +81 -1
- package/dist/src/lib/locks/lock-manager.js +230 -5
- package/dist/src/lib/locks/types.d.ts +72 -0
- package/dist/src/lib/locks/types.js +28 -0
- package/dist/src/lib/settings.d.ts +73 -0
- package/dist/src/lib/settings.js +45 -0
- package/dist/src/lib/test-tautology-detector.d.ts +4 -3
- package/dist/src/lib/test-tautology-detector.js +101 -41
- package/dist/src/lib/workflow/batch-executor.js +78 -19
- package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
- package/dist/src/lib/workflow/config-resolver.js +89 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/log-writer.d.ts +10 -1
- package/dist/src/lib/workflow/log-writer.js +20 -0
- package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.js +10 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
- package/dist/src/lib/workflow/ready-gate.js +24 -3
- package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
- package/dist/src/lib/workflow/run-log-schema.js +31 -1
- package/dist/src/lib/workflow/run-orchestrator.js +27 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/types.d.ts +64 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +9 -1
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/package.json +3 -2
- package/templates/hooks/pre-tool.sh +228 -0
- package/templates/scripts/cleanup-worktree.sh +36 -15
- package/templates/scripts/new-feature.sh +25 -19
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/assess/SKILL.md +5 -1
- package/templates/skills/exec/SKILL.md +61 -7
- package/templates/skills/fullsolve/SKILL.md +127 -21
- package/templates/skills/loop/SKILL.md +56 -11
- package/templates/skills/merger/SKILL.md +98 -10
- package/templates/skills/qa/SKILL.md +59 -6
- package/templates/skills/release/SKILL.md +79 -0
- package/templates/skills/spec/SKILL.md +31 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -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,6 +35,8 @@ 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
|
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
@@ -246,6 +258,14 @@ seg_match() {
|
|
|
246
258
|
[[ -n "$SEGMENTS" ]] && grep -qE "$1" <<< "$SEGMENTS"
|
|
247
259
|
}
|
|
248
260
|
|
|
261
|
+
# Path of the session->issue binding the checkout guard maintains (#906).
|
|
262
|
+
# $1 = repo toplevel, $2 = session id. The id is opaque, so squash everything
|
|
263
|
+
# outside a filename-safe set — it must not be able to escape the directory.
|
|
264
|
+
_co_binding_path() {
|
|
265
|
+
printf '%s/.sequant/locks/session-%s.issue' \
|
|
266
|
+
"$1" "$(printf '%s' "$2" | tr -c 'A-Za-z0-9_-' '_')"
|
|
267
|
+
}
|
|
268
|
+
|
|
249
269
|
# Precompute the segment list once, for Bash commands only.
|
|
250
270
|
SEGMENTS=""
|
|
251
271
|
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
@@ -458,6 +478,214 @@ if seg_match 'git reset.*(--hard|origin)'; then
|
|
|
458
478
|
fi
|
|
459
479
|
fi
|
|
460
480
|
|
|
481
|
+
# --- Session -> issue binding for the checkout guard (Issue #906) ---
|
|
482
|
+
# `SEQUANT_ISSUE` cannot identify the holder interactively, and never could:
|
|
483
|
+
# PreToolUse runs OUTSIDE and BEFORE the command's shell, so nothing a skill
|
|
484
|
+
# bash block exports is visible here — not even an export prepended to the same
|
|
485
|
+
# block as the guarded command. The one path that does export it (`sequant run`)
|
|
486
|
+
# also sets SEQUANT_ORCHESTRATOR, where this guard stands down. So the env
|
|
487
|
+
# fallback below was unreachable in every real flow, and the holder was
|
|
488
|
+
# routinely blocked by its own lock.
|
|
489
|
+
#
|
|
490
|
+
# The hook does see both the acquire and every later command of the same
|
|
491
|
+
# session, and `session_id` survives the shell boundary that kills the
|
|
492
|
+
# acquiring PID. Record the binding when we observe the acquire; read it back
|
|
493
|
+
# when deciding whether the caller is the holder.
|
|
494
|
+
if [[ -n "${SESSION_ID:-}" ]] && seg_match 'locks +checkout +(acquire|release)'; then
|
|
495
|
+
_CO_SB_ROOT=$(git -C "${HOOK_CWD:-$PWD}" rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
496
|
+
if [[ -n "$_CO_SB_ROOT" && -d "$_CO_SB_ROOT/.git" ]]; then
|
|
497
|
+
_CO_SB_FILE=$(_co_binding_path "$_CO_SB_ROOT" "$SESSION_ID")
|
|
498
|
+
_CO_SB_ISSUE=$(printf '%s' "$TOOL_INPUT" \
|
|
499
|
+
| grep -oE '\-\-issue[= ]+[0-9]+' | head -1 | grep -oE '[0-9]+$' || true)
|
|
500
|
+
if seg_match 'locks +checkout +acquire'; then
|
|
501
|
+
if [[ -n "$_CO_SB_ISSUE" ]]; then
|
|
502
|
+
mkdir -p "$(dirname "$_CO_SB_FILE")" 2>/dev/null \
|
|
503
|
+
&& printf '%s' "$_CO_SB_ISSUE" > "$_CO_SB_FILE" 2>/dev/null || true
|
|
504
|
+
fi
|
|
505
|
+
elif [[ -f "$_CO_SB_FILE" ]]; then
|
|
506
|
+
# Clear only when the session releases its OWN claim. A refused
|
|
507
|
+
# release (wrong --issue) must not strip the real holder's identity
|
|
508
|
+
# and leave it blocked by its own lock.
|
|
509
|
+
if [[ -n "$_CO_SB_ISSUE" \
|
|
510
|
+
&& "$_CO_SB_ISSUE" == "$(cat "$_CO_SB_FILE" 2>/dev/null)" ]]; then
|
|
511
|
+
rm -f "$_CO_SB_FILE" 2>/dev/null || true
|
|
512
|
+
fi
|
|
513
|
+
fi
|
|
514
|
+
fi
|
|
515
|
+
fi
|
|
516
|
+
|
|
517
|
+
# --- Checkout-scoped lock enforcement (Issue #901) ---
|
|
518
|
+
# The per-issue lock (#625) keys on issue number, so two sessions working
|
|
519
|
+
# *different* issues take different lock files and never contend. But
|
|
520
|
+
# `git checkout`, `switch`, `reset`, `rebase`, `merge` and `cherry-pick` are
|
|
521
|
+
# global to a working tree — the contended resource is the checkout, not the
|
|
522
|
+
# issue. `.sequant/locks/checkout.lock` represents the tree; this guard is what
|
|
523
|
+
# makes it binding, because the racing actor is an agent's Bash command, not
|
|
524
|
+
# sequant's TypeScript (which mutates git almost exclusively via `git -C
|
|
525
|
+
# <worktree>`).
|
|
526
|
+
#
|
|
527
|
+
# STALENESS IS A DELIBERATELY WEAKER SUBSET, NOT A MIRROR. The authoritative
|
|
528
|
+
# rules live in `classifyStaleness` (src/lib/locks/lock-manager.ts) and are
|
|
529
|
+
# shared by CheckoutLock. Transcribing them into shell would drift (#871), so
|
|
530
|
+
# this guard checks only the absolute age ceiling and FAILS OPEN past it. A
|
|
531
|
+
# lock this guard lets through is still caught by the TypeScript path; a lock
|
|
532
|
+
# it blocks on is always genuinely fresh. Weaker-but-honest beats a mirror.
|
|
533
|
+
#
|
|
534
|
+
# AC-5: orchestrator/MCP mode is a no-op here too, matching LockManager and
|
|
535
|
+
# CheckoutLock — `sequant run` drives its own worktree isolation and must not
|
|
536
|
+
# be blocked by a lock its own skills took.
|
|
537
|
+
if [[ -z "${SEQUANT_ORCHESTRATOR:-}" ]] \
|
|
538
|
+
&& seg_match 'git (checkout|switch|reset|rebase|merge|cherry-pick)( |$)' \
|
|
539
|
+
&& ! seg_match 'git +-C ' \
|
|
540
|
+
&& ! seg_match 'git checkout ([^ ]+ )?--( |$)'; then
|
|
541
|
+
|
|
542
|
+
# Only the MAIN checkout is protected — a command run inside a worktree
|
|
543
|
+
# touches only that worktree's HEAD and must never be blocked.
|
|
544
|
+
#
|
|
545
|
+
# Resolve where the command will ACTUALLY run. This must NOT use
|
|
546
|
+
# CLAUDE_PROJECT_DIR / PARALLEL_MARKER_PROJECT_ROOT: those name the
|
|
547
|
+
# *project* directory, which stays pinned to the main checkout even while
|
|
548
|
+
# the agent's shell sits in a worktree. Keying off them blocked legitimate
|
|
549
|
+
# in-worktree work — the guard's worst failure mode, since the whole point
|
|
550
|
+
# of the lock is to push sessions *into* worktrees.
|
|
551
|
+
#
|
|
552
|
+
# `.cwd` is part of Claude Code's PreToolUse envelope (verified against a
|
|
553
|
+
# live payload alongside `session_id`), with $PWD as the fallback.
|
|
554
|
+
_CO_CWD="${HOOK_CWD:-$PWD}"
|
|
555
|
+
# Honor a leading `cd <dir>` the same way the commit guard below does.
|
|
556
|
+
if echo "$TOOL_INPUT" | grep -qE '^cd [^;&|]+'; then
|
|
557
|
+
_CO_CD=$(echo "$TOOL_INPUT" | grep -oE '^cd [^;&|]+' | head -1 | sed 's/^cd //' | sed 's/[[:space:]]*$//')
|
|
558
|
+
[[ -n "$_CO_CD" && -d "$_CO_CD" ]] && _CO_CWD="$_CO_CD"
|
|
559
|
+
fi
|
|
560
|
+
|
|
561
|
+
# A linked worktree's toplevel has `.git` as a FILE; the main checkout has
|
|
562
|
+
# it as a directory.
|
|
563
|
+
_CO_ROOT=$(git -C "$_CO_CWD" rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
564
|
+
if [[ -n "$_CO_ROOT" && -d "$_CO_ROOT/.git" ]]; then
|
|
565
|
+
_CO_LOCK="$_CO_ROOT/.sequant/locks/checkout.lock"
|
|
566
|
+
|
|
567
|
+
if [[ -f "$_CO_LOCK" ]]; then
|
|
568
|
+
if command -v jq &>/dev/null; then
|
|
569
|
+
_CO_HOLDER_ISSUE=$(jq -r '.issue // empty' "$_CO_LOCK" 2>/dev/null)
|
|
570
|
+
_CO_HOLDER_SESSION=$(jq -r '.sessionId // empty' "$_CO_LOCK" 2>/dev/null)
|
|
571
|
+
_CO_HOLDER_PID=$(jq -r '.pid // empty' "$_CO_LOCK" 2>/dev/null)
|
|
572
|
+
_CO_HOLDER_HOST=$(jq -r '.hostname // empty' "$_CO_LOCK" 2>/dev/null)
|
|
573
|
+
_CO_HOLDER_STARTED=$(jq -r '.startedAt // empty' "$_CO_LOCK" 2>/dev/null)
|
|
574
|
+
_CO_HOLDER_CMD=$(jq -r '.command // empty' "$_CO_LOCK" 2>/dev/null)
|
|
575
|
+
else
|
|
576
|
+
_CO_HOLDER_ISSUE=$(grep -oE '"issue"[[:space:]]*:[[:space:]]*[0-9]+' "$_CO_LOCK" | head -1 | grep -oE '[0-9]+$')
|
|
577
|
+
_CO_HOLDER_SESSION=$(grep -oE '"sessionId"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
578
|
+
_CO_HOLDER_PID=$(grep -oE '"pid"[[:space:]]*:[[:space:]]*[0-9]+' "$_CO_LOCK" | head -1 | grep -oE '[0-9]+$')
|
|
579
|
+
_CO_HOLDER_HOST=$(grep -oE '"hostname"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
580
|
+
_CO_HOLDER_STARTED=$(grep -oE '"startedAt"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
581
|
+
_CO_HOLDER_CMD=$(grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' "$_CO_LOCK" | head -1 | cut -d'"' -f4)
|
|
582
|
+
fi
|
|
583
|
+
|
|
584
|
+
# Staleness. These branches mirror `classifyStaleness`
|
|
585
|
+
# (src/lib/locks/lock-manager.ts) in the same order, because AC-4
|
|
586
|
+
# requires the checkout lock's stale recovery to match the per-issue
|
|
587
|
+
# lock's — same-host dead PID, age ceiling, and the env overrides.
|
|
588
|
+
# Implementing only a subset here would let a *dead* holder block
|
|
589
|
+
# the tree for up to 24h, which is the wedge AC-4 forbids.
|
|
590
|
+
#
|
|
591
|
+
# The three rules are plain comparisons plus one `kill -0`, so this
|
|
592
|
+
# is a small enough surface to keep honest; the "hook/TypeScript
|
|
593
|
+
# staleness parity" cases in checkout-lock.integration.test.ts pin
|
|
594
|
+
# both sides to the same verdict so they cannot drift silently
|
|
595
|
+
# (#871 — the repo's drift guard compares literal strings only and
|
|
596
|
+
# would not see a semantic divergence here).
|
|
597
|
+
_CO_MAX_AGE_MS="${SEQUANT_MAX_LOCK_AGE_MS:-86400000}" # 24h ceiling
|
|
598
|
+
_CO_SKILL_TTL_MS="${SEQUANT_SKILL_LOCK_TTL_MS:-21600000}" # 6h skill-shell
|
|
599
|
+
_CO_STALE_AGE_MS=7200000 # 2h cross-host
|
|
600
|
+
_CO_FRESH=true
|
|
601
|
+
|
|
602
|
+
# `startedAt` is ISO-8601 **UTC**. BSD `date -j -f` parses in LOCAL
|
|
603
|
+
# time, so without TZ=UTC the age comes out shifted by the UTC
|
|
604
|
+
# offset — west of UTC that is *negative*, and a stale lock then
|
|
605
|
+
# reads as fresh forever, wedging the tree. TZ=UTC pins the BSD
|
|
606
|
+
# branch; Linux/CI falls through to GNU `date -u -d`, which honors
|
|
607
|
+
# the trailing Z.
|
|
608
|
+
_CO_AGE_MS=""
|
|
609
|
+
if [[ -n "$_CO_HOLDER_STARTED" ]]; then
|
|
610
|
+
_CO_STARTED_EPOCH=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "${_CO_HOLDER_STARTED%%.*}" +%s 2>/dev/null \
|
|
611
|
+
|| date -u -d "$_CO_HOLDER_STARTED" +%s 2>/dev/null || echo "")
|
|
612
|
+
if [[ -n "$_CO_STARTED_EPOCH" ]]; then
|
|
613
|
+
_CO_AGE_MS=$(( ( $(date +%s) - _CO_STARTED_EPOCH ) * 1000 ))
|
|
614
|
+
# Negative age = clock skew between hosts. Treat as unknown
|
|
615
|
+
# rather than stale: refusing is recoverable, silently
|
|
616
|
+
# ignoring a live holder is not.
|
|
617
|
+
[[ "$_CO_AGE_MS" -lt 0 ]] && _CO_AGE_MS=""
|
|
618
|
+
fi
|
|
619
|
+
fi
|
|
620
|
+
|
|
621
|
+
_CO_SKIP_PID=false
|
|
622
|
+
grep -q '"skipPidCheck"[[:space:]]*:[[:space:]]*true' "$_CO_LOCK" 2>/dev/null && _CO_SKIP_PID=true
|
|
623
|
+
|
|
624
|
+
# 0. Absolute ceiling, checked first and unconditionally (#856):
|
|
625
|
+
# past it a PID is no longer trustworthy identity.
|
|
626
|
+
if [[ -n "$_CO_AGE_MS" && "$_CO_AGE_MS" -gt "$_CO_MAX_AGE_MS" ]]; then
|
|
627
|
+
_CO_FRESH=false
|
|
628
|
+
# 1. Same-host PID check is authoritative — unless the holder asked
|
|
629
|
+
# us to skip it (a skill shell whose PID dies after acquire).
|
|
630
|
+
elif [[ "$_CO_HOLDER_HOST" == "$(hostname)" && "$_CO_SKIP_PID" == "false" ]]; then
|
|
631
|
+
# `kill -0` is a bash builtin: no subprocess on the hot path.
|
|
632
|
+
if [[ -n "$_CO_HOLDER_PID" ]] && ! kill -0 "$_CO_HOLDER_PID" 2>/dev/null; then
|
|
633
|
+
_CO_FRESH=false
|
|
634
|
+
fi
|
|
635
|
+
# 2. Cross-host or skipPidCheck: the PID is meaningless, use age.
|
|
636
|
+
elif [[ -n "$_CO_AGE_MS" ]]; then
|
|
637
|
+
if [[ "$_CO_SKIP_PID" == "true" ]]; then
|
|
638
|
+
_CO_TTL_MS="$_CO_SKILL_TTL_MS"
|
|
639
|
+
else
|
|
640
|
+
_CO_TTL_MS="$_CO_STALE_AGE_MS"
|
|
641
|
+
fi
|
|
642
|
+
[[ "$_CO_AGE_MS" -gt "$_CO_TTL_MS" ]] && _CO_FRESH=false
|
|
643
|
+
fi
|
|
644
|
+
|
|
645
|
+
# Is this session the holder? sessionId is the only identity that
|
|
646
|
+
# survives a skill shell exiting between acquire and this call, so
|
|
647
|
+
# it wins when both sides have one. Otherwise fall back to the
|
|
648
|
+
# issue this session is working on.
|
|
649
|
+
_CO_IS_HOLDER=false
|
|
650
|
+
if [[ -n "$_CO_HOLDER_SESSION" && -n "$SESSION_ID" ]]; then
|
|
651
|
+
[[ "$_CO_HOLDER_SESSION" == "$SESSION_ID" ]] && _CO_IS_HOLDER=true
|
|
652
|
+
elif [[ -n "${SEQUANT_ISSUE:-}" && -n "$_CO_HOLDER_ISSUE" ]]; then
|
|
653
|
+
# Reachable only from a parent process that exported it — never
|
|
654
|
+
# from a skill bash block (#906). Kept for `sequant run`-shaped
|
|
655
|
+
# callers; the binding below is what works interactively.
|
|
656
|
+
[[ "${SEQUANT_ISSUE}" == "$_CO_HOLDER_ISSUE" ]] && _CO_IS_HOLDER=true
|
|
657
|
+
elif [[ -n "${SESSION_ID:-}" && -n "$_CO_HOLDER_ISSUE" ]]; then
|
|
658
|
+
# The binding this hook recorded when it saw THIS session run
|
|
659
|
+
# `locks checkout acquire --issue=N` (#906).
|
|
660
|
+
_CO_BIND=$(_co_binding_path "$_CO_ROOT" "$SESSION_ID")
|
|
661
|
+
[[ -f "$_CO_BIND" \
|
|
662
|
+
&& "$(cat "$_CO_BIND" 2>/dev/null)" == "$_CO_HOLDER_ISSUE" ]] \
|
|
663
|
+
&& _CO_IS_HOLDER=true
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
if [[ "$_CO_FRESH" == "true" && "$_CO_IS_HOLDER" == "false" ]]; then
|
|
667
|
+
log_block "checkout-lock"
|
|
668
|
+
{
|
|
669
|
+
echo "HOOK_BLOCKED: Checkout held by another session"
|
|
670
|
+
echo ""
|
|
671
|
+
echo " The working tree is held by the session working #${_CO_HOLDER_ISSUE:-?}"
|
|
672
|
+
echo " (PID ${_CO_HOLDER_PID:-?} on ${_CO_HOLDER_HOST:-?}, started ${_CO_HOLDER_STARTED:-?})."
|
|
673
|
+
echo " Command: ${_CO_HOLDER_CMD:-?}"
|
|
674
|
+
echo ""
|
|
675
|
+
echo " Branch-mutating git here would race with that session."
|
|
676
|
+
echo ""
|
|
677
|
+
echo " To proceed:"
|
|
678
|
+
echo " • Work in your own worktree: ../worktrees/feature/<your-issue>-*/"
|
|
679
|
+
echo " (create it with: ./scripts/new-feature.sh <your-issue>)"
|
|
680
|
+
echo " • Or target it explicitly: git -C <worktree> <command>"
|
|
681
|
+
echo " • If that session is gone: sequant locks checkout clear --force"
|
|
682
|
+
} >&2
|
|
683
|
+
exit 2
|
|
684
|
+
fi
|
|
685
|
+
fi
|
|
686
|
+
fi
|
|
687
|
+
fi
|
|
688
|
+
|
|
461
689
|
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
462
690
|
if seg_match 'gh workflow run'; then
|
|
463
691
|
log_block "workflow-trigger"
|
|
@@ -357,26 +357,47 @@ else
|
|
|
357
357
|
echo -e "${YELLOW}⏭️ Skipped remote-branch delete (PR not merged; pass --delete-remote or --force to override).${NC}"
|
|
358
358
|
fi
|
|
359
359
|
|
|
360
|
-
#
|
|
360
|
+
# Refresh local main WITHOUT switching the main checkout's branch (#910). The
|
|
361
|
+
# old flow ran `git checkout main` here unconditionally — a branch switch of
|
|
362
|
+
# the shared main checkout, exactly the mutation the #901 checkout lock exists
|
|
363
|
+
# to serialize. The pre-tool guard cannot see it (the top-level command is this
|
|
364
|
+
# script, which carries no guarded git verb), so a session that lost the lock
|
|
365
|
+
# could still yank the holder off its branch — the same bypass #910 closed in
|
|
366
|
+
# new-feature.sh. Two cases:
|
|
367
|
+
# - already on main: update in place (fetch + ff-only pull; rebase only on
|
|
368
|
+
# divergence). Same landing point as before; no branch switch involved.
|
|
369
|
+
# - on any other branch (or detached HEAD): leave the checkout alone and
|
|
370
|
+
# fast-forward the local `main` REF via `git fetch origin main:main`,
|
|
371
|
+
# which never touches the working tree. When that is not fast-forwardable
|
|
372
|
+
# (local main diverged/ahead, or main is checked out in another worktree),
|
|
373
|
+
# report and skip — resolving that is the holder's call, not this script's.
|
|
361
374
|
echo -e "${BLUE}📥 Updating main branch...${NC}"
|
|
362
|
-
git checkout main
|
|
363
375
|
git fetch origin main
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if
|
|
367
|
-
#
|
|
368
|
-
if git merge-base --is-ancestor origin/main
|
|
369
|
-
# Local is
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
git
|
|
376
|
+
CURRENT_BRANCH=$(git symbolic-ref --quiet --short HEAD || echo "")
|
|
377
|
+
|
|
378
|
+
if [ "$CURRENT_BRANCH" = "main" ]; then
|
|
379
|
+
# Handle divergent branches gracefully
|
|
380
|
+
if ! git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then
|
|
381
|
+
# Local is behind or diverged - fast-forward or rebase
|
|
382
|
+
if git merge-base --is-ancestor origin/main HEAD 2>/dev/null; then
|
|
383
|
+
# Local is ahead - nothing to do
|
|
384
|
+
echo -e "${BLUE} Local main is ahead of origin${NC}"
|
|
385
|
+
else
|
|
386
|
+
# Diverged or behind - try fast-forward first
|
|
387
|
+
if ! git pull --ff-only origin main 2>/dev/null; then
|
|
388
|
+
echo -e "${YELLOW} Divergent branches detected, rebasing...${NC}"
|
|
389
|
+
git rebase origin/main
|
|
390
|
+
fi
|
|
376
391
|
fi
|
|
392
|
+
else
|
|
393
|
+
git pull --ff-only origin main 2>/dev/null || true
|
|
377
394
|
fi
|
|
378
395
|
else
|
|
379
|
-
git
|
|
396
|
+
if git fetch origin main:main 2>/dev/null; then
|
|
397
|
+
echo -e "${BLUE} Updated local main ref (checkout left on ${CURRENT_BRANCH:-detached HEAD})${NC}"
|
|
398
|
+
else
|
|
399
|
+
echo -e "${YELLOW} ⏭️ Skipped local main update (not fast-forwardable); checkout left on ${CURRENT_BRANCH:-detached HEAD}${NC}"
|
|
400
|
+
fi
|
|
380
401
|
fi
|
|
381
402
|
|
|
382
403
|
echo ""
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Create a new feature worktree from a GitHub issue
|
|
4
4
|
# Usage: ./scripts/new-feature.sh <issue-number> [--base <branch>] [--stash]
|
|
5
5
|
# Example: ./scripts/new-feature.sh 4
|
|
6
|
-
# Example: ./scripts/new-feature.sh 4 --stash #
|
|
6
|
+
# Example: ./scripts/new-feature.sh 4 --stash # Deprecated no-op (kept for compatibility)
|
|
7
7
|
# Example: ./scripts/new-feature.sh 4 --base feature/dashboard # Branch from feature branch
|
|
8
8
|
|
|
9
9
|
set -e
|
|
@@ -108,18 +108,14 @@ echo -e "${BLUE}Base: ${BASE_BRANCH}${NC}"
|
|
|
108
108
|
echo -e "${BLUE}Worktree: ${WORKTREE_DIR}${NC}"
|
|
109
109
|
echo ""
|
|
110
110
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
echo -e "${YELLOW} Use --stash to auto-stash, or manually:${NC}"
|
|
120
|
-
echo -e " git stash push -m 'WIP before issue #${ISSUE_NUMBER}'"
|
|
121
|
-
exit 1
|
|
122
|
-
fi
|
|
111
|
+
# The main checkout is no longer switched or updated by this script (#910), so
|
|
112
|
+
# an uncommitted main working tree can no longer block or endanger worktree
|
|
113
|
+
# creation — the worktree is branched directly off the fetched remote ref
|
|
114
|
+
# below, and `git fetch` / `git worktree add` both operate happily on a dirty
|
|
115
|
+
# main tree. `--stash` is therefore obsolete: still accepted for backward
|
|
116
|
+
# compatibility, but a no-op, since there is nothing to move out of the way.
|
|
117
|
+
if [ "$STASH_FLAG" = true ]; then
|
|
118
|
+
echo -e "${YELLOW}ℹ️ --stash is deprecated and now does nothing: new-feature.sh no longer touches the main checkout, so its uncommitted changes are left in place.${NC}"
|
|
123
119
|
fi
|
|
124
120
|
|
|
125
121
|
# Check if branch already exists
|
|
@@ -142,15 +138,25 @@ if git show-ref --verify --quiet "refs/heads/${BRANCH_NAME}"; then
|
|
|
142
138
|
fi
|
|
143
139
|
fi
|
|
144
140
|
|
|
145
|
-
#
|
|
146
|
-
|
|
141
|
+
# Refresh the base branch's remote-tracking ref WITHOUT touching the main
|
|
142
|
+
# checkout. The old flow — `git checkout <base>` then `git pull` — switched the
|
|
143
|
+
# main checkout's branch and mutated its working tree, a global operation the
|
|
144
|
+
# #901 checkout lock exists to serialize. But the pre-tool guard inspects only
|
|
145
|
+
# the top-level Bash command the agent runs; that command is `new-feature.sh`,
|
|
146
|
+
# which carries no guarded git verb, so the inner `checkout`/`pull` sailed past
|
|
147
|
+
# the guard and a losing session could switch the holder's branch out from
|
|
148
|
+
# under it (#910). Reading the ref instead of checking it out closes the gap at
|
|
149
|
+
# its root: the main tree is never mutated, so no lock — and no guard — is
|
|
150
|
+
# needed here at all.
|
|
151
|
+
echo -e "${BLUE}📥 Fetching ${BASE_BRANCH} from origin...${NC}"
|
|
147
152
|
git fetch origin "$BASE_BRANCH"
|
|
148
|
-
git checkout "$BASE_BRANCH"
|
|
149
|
-
git pull origin "$BASE_BRANCH"
|
|
150
153
|
|
|
151
|
-
#
|
|
154
|
+
# Branch the worktree directly off the freshly fetched remote ref.
|
|
155
|
+
# `git worktree add -b <new> <path> <start-point>` resolves <start-point>
|
|
156
|
+
# without changing the main checkout, and `origin/<base>` is exactly the
|
|
157
|
+
# up-to-date commit the old `checkout`+`pull` used to land on.
|
|
152
158
|
echo -e "${BLUE}🌿 Creating new worktree from ${BASE_BRANCH}...${NC}"
|
|
153
|
-
git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME"
|
|
159
|
+
git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME" "origin/${BASE_BRANCH}"
|
|
154
160
|
|
|
155
161
|
# Record the base branch on the new branch so downstream tooling
|
|
156
162
|
# (e.g. phase-executor zero-diff guard, see #537) can resolve the
|
|
@@ -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
|
|
|
@@ -29,6 +29,7 @@ allowed-tools:
|
|
|
29
29
|
# Worktree management
|
|
30
30
|
- Bash(./scripts/new-feature.sh:*)
|
|
31
31
|
- Bash(./scripts/cleanup-worktree.sh:*)
|
|
32
|
+
- Bash(npx sequant worktree:*)
|
|
32
33
|
# GitHub CLI
|
|
33
34
|
- Bash(gh issue view:*)
|
|
34
35
|
- Bash(gh issue comment:*)
|
|
@@ -136,7 +137,7 @@ When running as part of an orchestrated workflow (e.g., `sequant run` or `/fulls
|
|
|
136
137
|
|
|
137
138
|
1. **Skip pre-flight git checks** - The orchestrator has already verified git state
|
|
138
139
|
2. **Skip worktree creation** - Orchestrator creates worktrees before invoking skills
|
|
139
|
-
3. **
|
|
140
|
+
3. **Verify, then use, the provided path** - `SEQUANT_WORKTREE` is authoritative *when valid*, but it is never trusted unchecked: run the existence guard in "Feature Worktree Workflow" below and halt if it fails (#899)
|
|
140
141
|
4. **Reduce GitHub comment frequency** - Defer progress updates to the orchestrator
|
|
141
142
|
5. **Trust issue context** - The orchestrator has already fetched and validated issue data
|
|
142
143
|
|
|
@@ -472,15 +473,68 @@ echo "Current branch: $CURRENT_BRANCH"
|
|
|
472
473
|
**Why this matters:** Work done directly on main can be lost during sync operations (git reset, git pull --rebase, etc.). Worktrees provide isolation and safe recovery through branches.
|
|
473
474
|
|
|
474
475
|
**If orchestrated (SEQUANT_WORKTREE is set):**
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
476
|
+
|
|
477
|
+
<!-- BEGIN: worktree-existence-guard (#899) -->
|
|
478
|
+
|
|
479
|
+
**Verify the path before you use it. Never `cd` into it unchecked.** An
|
|
480
|
+
orchestrator can hand over a path that was never created, or one that resolves
|
|
481
|
+
into a *different repository's* worktree — `../worktrees/` is one flat
|
|
482
|
+
namespace shared by every repo under the same parent, and issue numbers are
|
|
483
|
+
per-repo. A bare `cd` fails silently and leaves you implementing in the main
|
|
484
|
+
checkout, on whatever branch it happens to be on.
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
npx sequant worktree verify "$SEQUANT_WORKTREE" --issue <issue-number> || {
|
|
488
|
+
echo "❌ HALT: SEQUANT_WORKTREE is not a usable worktree of this repository."
|
|
489
|
+
exit 1
|
|
490
|
+
}
|
|
491
|
+
cd "$SEQUANT_WORKTREE"
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`verify` exits non-zero with one of these named errors. **Every one of them is
|
|
495
|
+
a halt** — report it and stop; do not fall back to creating a worktree, and do
|
|
496
|
+
not continue in the current directory:
|
|
497
|
+
|
|
498
|
+
| Error | Meaning |
|
|
499
|
+
|-------|---------|
|
|
500
|
+
| `SEQUANT_WORKTREE_NOT_FOUND` | Path is empty, an unexpanded glob, or not an existing directory |
|
|
501
|
+
| `SEQUANT_WORKTREE_FOREIGN` | Real directory, but not a worktree of *this* repository (another project's, or stale) |
|
|
502
|
+
| `SEQUANT_WORKTREE_ISSUE_MISMATCH` | A worktree of this repo, but its branch belongs to a different issue |
|
|
503
|
+
|
|
504
|
+
Once verify passes, skip steps 1-2 below and continue with step 3 (Work in the
|
|
505
|
+
worktree).
|
|
506
|
+
|
|
507
|
+
<!-- END: worktree-existence-guard (#899) -->
|
|
478
508
|
|
|
479
509
|
**If standalone:**
|
|
480
510
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
511
|
+
<!-- BEGIN: worktree-standalone-lookup (#899) -->
|
|
512
|
+
|
|
513
|
+
1. **Check if a worktree already exists for this issue:**
|
|
514
|
+
|
|
515
|
+
Resolve through git, not the filesystem. `sequant worktree resolve` reads
|
|
516
|
+
`git worktree list` in the current repository — which reports only *this*
|
|
517
|
+
repo's worktrees — and selects on the **branch** git reports, not on the
|
|
518
|
+
directory name. That matters twice over: a directory slug is shared across
|
|
519
|
+
repositories, and it can drift from its own branch after a rename
|
|
520
|
+
(`578-seo-expand-city-coverage` holding branch `feature/578-city-expansion-clean`),
|
|
521
|
+
so a name match proves nothing about which branch you would land on.
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
if WORKTREE="$(npx sequant worktree resolve <issue-number>)"; then
|
|
525
|
+
echo "Existing worktree: $WORKTREE"
|
|
526
|
+
cd "$WORKTREE"
|
|
527
|
+
# Continue work there — skip step 2.
|
|
528
|
+
else
|
|
529
|
+
echo "No worktree for #<issue-number> in this repository — create one (step 2)."
|
|
530
|
+
fi
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Do not glob `../worktrees/feature/<issue-number>-*` to find it.** That
|
|
534
|
+
directory is shared by every sibling repository, so the match may belong to
|
|
535
|
+
another project entirely.
|
|
536
|
+
|
|
537
|
+
<!-- END: worktree-standalone-lookup (#899) -->
|
|
484
538
|
|
|
485
539
|
2. **Create worktree if needed (with parallel context gathering):**
|
|
486
540
|
|