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.
Files changed (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +6 -2
  3. package/dist/bin/cli.js +47 -2
  4. package/dist/src/commands/locks.d.ts +20 -1
  5. package/dist/src/commands/locks.js +206 -4
  6. package/dist/src/commands/ready.d.ts +6 -0
  7. package/dist/src/commands/ready.js +15 -1
  8. package/dist/src/commands/run-display.js +1 -0
  9. package/dist/src/commands/worktree.d.ts +31 -0
  10. package/dist/src/commands/worktree.js +95 -0
  11. package/dist/src/lib/cli-flags.d.ts +23 -0
  12. package/dist/src/lib/cli-flags.js +43 -0
  13. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  14. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  15. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  16. package/dist/src/lib/locks/checkout-lock.js +389 -0
  17. package/dist/src/lib/locks/index.d.ts +6 -3
  18. package/dist/src/lib/locks/index.js +4 -2
  19. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  20. package/dist/src/lib/locks/lock-manager.js +230 -5
  21. package/dist/src/lib/locks/types.d.ts +72 -0
  22. package/dist/src/lib/locks/types.js +28 -0
  23. package/dist/src/lib/settings.d.ts +73 -0
  24. package/dist/src/lib/settings.js +45 -0
  25. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  26. package/dist/src/lib/test-tautology-detector.js +101 -41
  27. package/dist/src/lib/workflow/batch-executor.js +78 -19
  28. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  29. package/dist/src/lib/workflow/config-resolver.js +89 -0
  30. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
  31. package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
  32. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  33. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  34. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  35. package/dist/src/lib/workflow/error-classifier.js +4 -0
  36. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  37. package/dist/src/lib/workflow/log-writer.js +20 -0
  38. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  39. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  40. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  41. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  42. package/dist/src/lib/workflow/phase-detection.js +5 -1
  43. package/dist/src/lib/workflow/phase-executor.js +10 -0
  44. package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
  45. package/dist/src/lib/workflow/ready-gate.js +24 -3
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +31 -1
  48. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  49. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  50. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  51. package/dist/src/lib/workflow/types.d.ts +64 -0
  52. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  53. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  54. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  55. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  56. package/package.json +3 -2
  57. package/templates/hooks/pre-tool.sh +228 -0
  58. package/templates/scripts/cleanup-worktree.sh +36 -15
  59. package/templates/scripts/new-feature.sh +25 -19
  60. package/templates/skills/_shared/references/subagent-types.md +7 -18
  61. package/templates/skills/assess/SKILL.md +5 -1
  62. package/templates/skills/exec/SKILL.md +61 -7
  63. package/templates/skills/fullsolve/SKILL.md +127 -21
  64. package/templates/skills/loop/SKILL.md +56 -11
  65. package/templates/skills/merger/SKILL.md +98 -10
  66. package/templates/skills/qa/SKILL.md +59 -6
  67. package/templates/skills/release/SKILL.md +79 -0
  68. package/templates/skills/spec/SKILL.md +31 -15
  69. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  70. package/templates/skills/testgen/SKILL.md +23 -6
  71. 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
- # Update main
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
- # Handle divergent branches gracefully
366
- if ! git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then
367
- # Local is behind or diverged - fast-forward or rebase
368
- if git merge-base --is-ancestor origin/main HEAD 2>/dev/null; then
369
- # Local is ahead - nothing to do
370
- echo -e "${BLUE} Local main is ahead of origin${NC}"
371
- else
372
- # Diverged or behind - try fast-forward first
373
- if ! git pull --ff-only origin main 2>/dev/null; then
374
- echo -e "${YELLOW} Divergent branches detected, rebasing...${NC}"
375
- git rebase origin/main
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 pull --ff-only origin main 2>/dev/null || true
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 # Auto-stash uncommitted changes
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
- # Check for uncommitted changes before switching branches
112
- if ! git diff-index --quiet HEAD -- 2>/dev/null; then
113
- if [ "$STASH_FLAG" = true ]; then
114
- echo -e "${YELLOW}📦 Stashing uncommitted changes...${NC}"
115
- git stash push --include-untracked -m "WIP before issue #${ISSUE_NUMBER}"
116
- echo -e "${GREEN} Changes stashed successfully${NC}"
117
- else
118
- echo -e "${RED} Working tree has uncommitted changes${NC}"
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
- # Update base branch
146
- echo -e "${BLUE}📥 Updating ${BASE_BRANCH} branch...${NC}"
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
- # Create worktree from base branch
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 **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
 
@@ -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. **Use provided worktree path** - Work in `SEQUANT_WORKTREE` instead of creating a new one
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
- - Use the provided worktree path directly: `cd $SEQUANT_WORKTREE`
476
- - Skip steps 1-2 below (worktree already created by orchestrator)
477
- - Continue with step 3 (Work in the worktree)
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
- 1. **Check if worktree already exists:**
482
- - Check if you're already in a worktree: `git worktree list` or check if `../worktrees/` contains a directory for this issue
483
- - If worktree exists, navigate to it and continue work there
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