sequant 2.9.0 → 2.10.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 (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +14 -3
  4. package/dist/bin/cli.js +97 -17
  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 +56 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.js +2 -0
  36. package/dist/src/commands/logs.js +14 -5
  37. package/dist/src/commands/merge.js +154 -2
  38. package/dist/src/commands/ready-tui-adapter.js +6 -1
  39. package/dist/src/commands/ready.d.ts +25 -3
  40. package/dist/src/commands/ready.js +39 -13
  41. package/dist/src/commands/resume.d.ts +113 -0
  42. package/dist/src/commands/resume.js +193 -0
  43. package/dist/src/commands/run-display.js +37 -4
  44. package/dist/src/commands/run-flags.d.ts +42 -1
  45. package/dist/src/commands/run-flags.js +53 -1
  46. package/dist/src/commands/run-progress.js +26 -1
  47. package/dist/src/commands/run.js +11 -10
  48. package/dist/src/commands/state.js +7 -0
  49. package/dist/src/commands/status.d.ts +9 -0
  50. package/dist/src/commands/status.js +24 -1
  51. package/dist/src/commands/sync.js +90 -19
  52. package/dist/src/commands/update.js +28 -5
  53. package/dist/src/lib/ac-parser.d.ts +14 -0
  54. package/dist/src/lib/ac-parser.js +99 -5
  55. package/dist/src/lib/assess/index.d.ts +10 -0
  56. package/dist/src/lib/assess/index.js +9 -0
  57. package/dist/src/lib/assess/renderer.d.ts +64 -0
  58. package/dist/src/lib/assess/renderer.js +481 -0
  59. package/dist/src/lib/assess/types.d.ts +224 -0
  60. package/dist/src/lib/assess/types.js +241 -0
  61. package/dist/src/lib/ci/outputs.d.ts +15 -2
  62. package/dist/src/lib/ci/outputs.js +17 -5
  63. package/dist/src/lib/cli-flags.d.ts +33 -0
  64. package/dist/src/lib/cli-flags.js +46 -0
  65. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  66. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  67. package/dist/src/lib/cli-ui/run-renderer.js +62 -0
  68. package/dist/src/lib/errors.d.ts +78 -6
  69. package/dist/src/lib/errors.js +133 -15
  70. package/dist/src/lib/locks/index.d.ts +3 -3
  71. package/dist/src/lib/locks/index.js +2 -2
  72. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  73. package/dist/src/lib/locks/lock-manager.js +47 -3
  74. package/dist/src/lib/locks/types.d.ts +34 -3
  75. package/dist/src/lib/locks/types.js +16 -0
  76. package/dist/src/lib/mcp-config.d.ts +45 -0
  77. package/dist/src/lib/mcp-config.js +77 -1
  78. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  79. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  80. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  81. package/dist/src/lib/merge-check/command-result.js +56 -0
  82. package/dist/src/lib/merge-check/index.d.ts +4 -0
  83. package/dist/src/lib/merge-check/index.js +1 -1
  84. package/dist/src/lib/merge-check/report.js +5 -1
  85. package/dist/src/lib/merge-check/types.d.ts +9 -0
  86. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  87. package/dist/src/lib/merge-check/watch.js +215 -0
  88. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  89. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  90. package/dist/src/lib/settings.d.ts +14 -0
  91. package/dist/src/lib/settings.js +61 -8
  92. package/dist/src/lib/shutdown.d.ts +42 -2
  93. package/dist/src/lib/shutdown.js +75 -5
  94. package/dist/src/lib/skills-check.d.ts +26 -0
  95. package/dist/src/lib/skills-check.js +31 -0
  96. package/dist/src/lib/stacks.d.ts +110 -1
  97. package/dist/src/lib/stacks.js +244 -5
  98. package/dist/src/lib/templates.d.ts +74 -0
  99. package/dist/src/lib/templates.js +171 -16
  100. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  101. package/dist/src/lib/test-tautology-detector.js +153 -12
  102. package/dist/src/lib/version-check.d.ts +31 -0
  103. package/dist/src/lib/version-check.js +45 -2
  104. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  105. package/dist/src/lib/workflow/batch-executor.js +477 -44
  106. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  107. package/dist/src/lib/workflow/chain-resume.js +24 -20
  108. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  109. package/dist/src/lib/workflow/completed-status.js +57 -0
  110. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  111. package/dist/src/lib/workflow/config-resolver.js +54 -2
  112. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  113. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  114. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  115. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  116. package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
  117. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  118. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  119. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  120. package/dist/src/lib/workflow/heartbeat.js +90 -0
  121. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  122. package/dist/src/lib/workflow/log-writer.js +75 -6
  123. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  124. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  125. package/dist/src/lib/workflow/phase-executor.js +457 -52
  126. package/dist/src/lib/workflow/phase-registry.js +2 -1
  127. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  128. package/dist/src/lib/workflow/platforms/github.js +152 -0
  129. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  130. package/dist/src/lib/workflow/qa-cache.js +22 -9
  131. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  132. package/dist/src/lib/workflow/ready-gate.js +25 -9
  133. package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
  134. package/dist/src/lib/workflow/run-log-schema.js +47 -1
  135. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  136. package/dist/src/lib/workflow/run-orchestrator.js +141 -20
  137. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  138. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  139. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  140. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  141. package/dist/src/lib/workflow/state-manager.js +83 -0
  142. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  143. package/dist/src/lib/workflow/state-schema.js +47 -0
  144. package/dist/src/lib/workflow/types.d.ts +126 -4
  145. package/dist/src/lib/workflow/types.js +1 -0
  146. package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
  147. package/dist/src/lib/workflow/worktree-manager.js +102 -23
  148. package/dist/src/mcp/tools/run.d.ts +9 -1
  149. package/dist/src/mcp/tools/run.js +20 -1
  150. package/dist/src/mcp/tools/status.js +6 -0
  151. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  152. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  153. package/dist/src/ui/tui/IssueBox.js +10 -2
  154. package/dist/src/ui/tui/row-cap.js +2 -1
  155. package/dist/src/ui/tui/theme.d.ts +7 -0
  156. package/dist/src/ui/tui/theme.js +9 -0
  157. package/package.json +8 -7
  158. package/templates/hooks/parallel-marker.sh +50 -0
  159. package/templates/hooks/post-tool.sh +20 -7
  160. package/templates/hooks/pre-tool.sh +75 -10
  161. package/templates/scripts/cleanup-worktree.sh +182 -19
  162. package/templates/scripts/new-feature.sh +264 -16
  163. package/templates/skills/_shared/references/trust-model.md +18 -0
  164. package/templates/skills/assess/SKILL.md +215 -390
  165. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  166. package/templates/skills/docs/SKILL.md +4 -4
  167. package/templates/skills/exec/SKILL.md +26 -6
  168. package/templates/skills/fullsolve/SKILL.md +3 -1
  169. package/templates/skills/loop/SKILL.md +38 -2
  170. package/templates/skills/merger/SKILL.md +2 -2
  171. package/templates/skills/qa/SKILL.md +337 -57
  172. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  173. package/templates/skills/qa/references/call-site-review.md +2 -2
  174. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  175. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  176. package/templates/skills/qa/references/quality-gates.md +21 -7
  177. package/templates/skills/qa/references/testing-requirements.md +1 -1
  178. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  179. package/templates/skills/release/SKILL.md +1 -1
  180. package/templates/skills/setup/SKILL.md +5 -5
  181. package/templates/skills/spec/SKILL.md +2 -0
  182. package/templates/skills/test/SKILL.md +1 -1
  183. package/dist/src/lib/phase-spinner.d.ts +0 -146
  184. package/dist/src/lib/phase-spinner.js +0 -255
  185. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  186. package/dist/src/lib/workflow/pr-operations.js +0 -326
  187. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  188. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -60,7 +60,23 @@ mkdir -p "$_LOG_DIR" 2>/dev/null || _LOG_DIR="${_TMPDIR}"
60
60
 
61
61
  TIMING_LOG="${_LOG_DIR}/claude-timing.log"
62
62
  HOOK_LOG="${_LOG_DIR}/claude-hook.log"
63
- PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
63
+
64
+ # Parallel-group marker naming (#881). The scheme lives in one sourced helper
65
+ # next to this hook so the writer (/exec skill) and the readers (this hook and
66
+ # post-tool.sh) cannot drift. PARALLEL_MARKER_PREFIX is project-scoped, so a
67
+ # parallel group in another project no longer collides in the shared temp dir.
68
+ _MARKER_HELPER="$(dirname "${BASH_SOURCE[0]:-$0}")/parallel-marker.sh"
69
+ if [[ -f "$_MARKER_HELPER" ]]; then
70
+ # shellcheck source=parallel-marker.sh disable=SC1091
71
+ source "$_MARKER_HELPER"
72
+ PARALLEL_MARKER_PREFIX="$(parallel_marker_prefix)"
73
+ PARALLEL_MARKER_PROJECT_ROOT="$(parallel_marker_project_root)"
74
+ else
75
+ # Helper missing (unexpected): fall back to the pre-#881 global prefix so the
76
+ # hook still functions, accepting the old cross-project collision risk.
77
+ PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
78
+ PARALLEL_MARKER_PROJECT_ROOT=""
79
+ fi
64
80
 
65
81
  # === HELPERS ===
66
82
 
@@ -237,17 +253,20 @@ if [[ "$TOOL_NAME" == "Bash" ]]; then
237
253
  fi
238
254
 
239
255
  # === AGENT ID DETECTION ===
240
- # For parallel agents, detect group ID from marker files
241
- # Format: ${_TMPDIR}/claude-parallel-<group-id>.marker
256
+ # For parallel agents, detect group ID from marker files. The glob is scoped to
257
+ # the current project's marker prefix (#881) so a foreign project's marker never
258
+ # labels this session's timing rows.
242
259
  AGENT_ID=""
260
+ _MARKER_BASE=$(basename "$PARALLEL_MARKER_PREFIX")
243
261
  # Find marker files using find (works in both bash and zsh)
244
262
  while IFS= read -r marker; do
245
263
  if [[ -n "$marker" && -f "$marker" ]]; then
246
- # Extract group ID from marker filename
247
- AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.marker//')
264
+ # Extract group ID: strip the project-scoped prefix and the suffix.
265
+ AGENT_ID=$(basename "$marker" .marker)
266
+ AGENT_ID=${AGENT_ID#"$_MARKER_BASE"}
248
267
  break
249
268
  fi
250
- done < <(find "${_TMPDIR}" -maxdepth 1 -name "claude-parallel-*.marker" 2>/dev/null)
269
+ done < <(find "${_TMPDIR}" -maxdepth 1 -name "${_MARKER_BASE}*.marker" 2>/dev/null)
251
270
 
252
271
  # === TIMING START ===
253
272
  # Include agent ID in log format if available (AC-4)
@@ -617,6 +636,36 @@ if [[ "$TOOL_NAME" == "Bash" ]] && seg_match 'git commit'; then
617
636
  fi
618
637
  fi
619
638
 
639
+ # Resolve a path to its canonical form, tolerating components that do not exist
640
+ # yet. Plain `realpath` fails outright on a missing path, and BSD/macOS realpath
641
+ # has no `-m`. The old code fell back to the raw string on failure, which broke
642
+ # the comparison below whenever the worktree path crossed a symlink: a Write
643
+ # creating a NEW file resolved to nothing (raw "/tmp/.../x.ts") while the
644
+ # existing worktree dir resolved through the symlink ("/private/tmp/.../"), so
645
+ # the prefix test failed and writes *inside* the worktree were blocked. macOS
646
+ # hits this every time, since /tmp is a symlink to /private/tmp.
647
+ #
648
+ # Walk up to the nearest existing ancestor, canonicalise that, then re-append
649
+ # the missing tail so both sides normalise identically.
650
+ resolve_path_allow_missing() {
651
+ local p="$1"
652
+ local suffix=""
653
+
654
+ while [[ ! -e "$p" && "$p" != "/" && "$p" != "." && -n "$p" ]]; do
655
+ suffix="/$(basename "$p")$suffix"
656
+ local parent
657
+ parent="$(dirname "$p")"
658
+ [[ "$parent" == "$p" ]] && break
659
+ p="$parent"
660
+ done
661
+
662
+ local resolved
663
+ resolved="$(realpath "$p" 2>/dev/null || echo "$p")"
664
+ # Avoid a doubled slash when the resolved ancestor is "/"
665
+ [[ "$resolved" == "/" ]] && resolved=""
666
+ echo "${resolved}${suffix}"
667
+ }
668
+
620
669
  # === WORKTREE PATH ENFORCEMENT ===
621
670
  # Enforces that file operations stay within the designated worktree
622
671
  # Sources for worktree path (in priority order):
@@ -631,10 +680,21 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
631
680
  EXPECTED_WORKTREE="$SEQUANT_WORKTREE"
632
681
  fi
633
682
 
634
- # Priority 2: Fall back to parallel marker file
683
+ # Priority 2: Fall back to parallel marker file. The glob prefix is already
684
+ # scoped to this project's hash (#881, AC-2), so a foreign project's marker
685
+ # is not matched here. As defense-in-depth (AC-3), also verify the marker's
686
+ # stored owning project root (line 2) matches the current one before trusting
687
+ # it — a stale or hand-placed marker with a colliding name is ignored rather
688
+ # than allowed to redirect enforcement.
635
689
  if [[ -z "$EXPECTED_WORKTREE" ]]; then
636
690
  for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
637
691
  if [[ -f "$marker" ]]; then
692
+ MARKER_PROJECT_ROOT=$(sed -n '2p' "$marker" 2>/dev/null || true)
693
+ if [[ -n "$MARKER_PROJECT_ROOT" && -n "$PARALLEL_MARKER_PROJECT_ROOT" \
694
+ && "$MARKER_PROJECT_ROOT" != "$PARALLEL_MARKER_PROJECT_ROOT" ]]; then
695
+ # Foreign owner — ignore this marker entirely.
696
+ continue
697
+ fi
638
698
  # Read expected worktree path from marker file (first line)
639
699
  EXPECTED_WORKTREE=$(head -1 "$marker" 2>/dev/null || true)
640
700
  break
@@ -660,9 +720,14 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
660
720
  fi
661
721
 
662
722
  if [[ -n "$FILE_PATH" ]]; then
663
- # Resolve to absolute path for consistent comparison
664
- REAL_FILE_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
665
- REAL_WORKTREE=$(realpath "$EXPECTED_WORKTREE" 2>/dev/null || echo "$EXPECTED_WORKTREE")
723
+ # Resolve to canonical absolute paths for consistent comparison.
724
+ # resolve_path_allow_missing (defined above) tolerates a file that
725
+ # does not exist yet; a bare `realpath` fallback-to-raw-string broke
726
+ # new-file Writes whenever the worktree path crossed a symlink
727
+ # (macOS /tmp -> /private/tmp): the new file stayed raw while the
728
+ # existing worktree dir resolved, so the prefix test always failed.
729
+ REAL_FILE_PATH=$(resolve_path_allow_missing "$FILE_PATH")
730
+ REAL_WORKTREE=$(resolve_path_allow_missing "$EXPECTED_WORKTREE")
666
731
 
667
732
  # Check if file path is within the expected worktree
668
733
  if [[ "$REAL_FILE_PATH" != "$REAL_WORKTREE"* ]]; then
@@ -1,7 +1,17 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # Clean up a worktree after PR is merged
4
- # Usage: ./scripts/cleanup-worktree.sh [flags] <branch-name>
4
+ # Usage: ./scripts/cleanup-worktree.sh [flags] <branch>
5
+ # <branch> accepts a bare issue number (`123`), a QUOTED glob
6
+ # (`'feature/123-*'`), or the full branch name — all resolve to the same
7
+ # worktree and, since #838, to the same underlying branch ref.
8
+ # A bare number is ANCHORED to the issue-number position (#844): `838` resolves
9
+ # `feature/838-*` and never `feature/1838-other-work`. When the argument matches
10
+ # more than one worktree branch the script lists every candidate and exits
11
+ # non-zero instead of silently deleting the first.
12
+ # Quote the glob: this script resolves the pattern itself, so it must arrive
13
+ # unexpanded. zsh (the default macOS shell) aborts on an unmatched unquoted
14
+ # glob before the script ever runs; bash would pass it through literally.
5
15
  # Example: ./scripts/cleanup-worktree.sh feature/123-add-user-dashboard
6
16
  #
7
17
  # The remote branch is deleted ONLY when the branch's PR is MERGED (the
@@ -28,7 +38,20 @@ NC='\033[0m'
28
38
  # Print CLI usage/help (kept in sync with the header comment above).
29
39
  print_usage() {
30
40
  cat <<'USAGE'
31
- Usage: ./scripts/cleanup-worktree.sh [flags] <branch-name>
41
+ Usage: ./scripts/cleanup-worktree.sh [flags] <branch>
42
+
43
+ <branch> may be any of (all resolve to the same worktree):
44
+ 838 a bare issue number — anchored to the issue-number
45
+ position, so it never matches feature/1838-other-work
46
+ 'feature/838-*' a glob — MUST be quoted (see below)
47
+ feature/838-fix-thing the full branch name
48
+
49
+ An argument that matches more than one worktree branch is rejected: the script
50
+ lists every candidate and exits non-zero rather than deleting the first.
51
+
52
+ Quote the glob form. This script resolves the pattern itself, so it must
53
+ arrive unexpanded; zsh aborts on an unmatched unquoted glob before the
54
+ script ever runs.
32
55
 
33
56
  Clean up a feature worktree. The remote branch is deleted ONLY when the
34
57
  branch's PR is MERGED (the documented post-merge contract) or when an
@@ -101,17 +124,149 @@ if [ -z "$BRANCH_NAME" ]; then
101
124
  exit 1
102
125
  fi
103
126
 
104
- # Find worktree path. Parse porcelain (which separates path/branch onto distinct
105
- # lines) so paths containing whitespace survive intact, and so we match against
106
- # the branch ref rather than against a free-form `grep` over the entire line
107
- # the latter false-matches whenever the branch name appears in the path string.
108
- WORKTREE_PATH=$(git worktree list --porcelain | awk -v target="$BRANCH_NAME" '
109
- /^worktree / { sub(/^worktree /, ""); path = $0 }
110
- /^branch refs\/heads\// {
111
- sub(/^branch refs\/heads\//, "")
112
- if (index($0, target) > 0) { print path; exit }
113
- }
114
- ')
127
+ # Resolve $BRANCH_NAME to exactly one worktree AND its full branch ref. Both are
128
+ # captured because every downstream consumer (`gh pr list --head`, `git branch
129
+ # -D`, `git push origin --delete`) needs the EXACT ref, or it silently no-ops on
130
+ # a shorthand and the script prints "Cleanup complete!" while leaving the branch
131
+ # behind (the #838 defect $RESOLVED_BRANCH closes).
132
+ #
133
+ # Matching is TIERED and anchored (#844). The old matcher was a single
134
+ # unanchored substring test with first-match-wins and no ambiguity check:
135
+ #
136
+ # case "$_wt_branch" in *"$BRANCH_NAME"* | $BRANCH_NAME)
137
+ #
138
+ # so `838` matched `feature/1838-other-work`, and whichever worktree porcelain
139
+ # emitted first was torn down — local AND remote, since #838. Porcelain sorts
140
+ # linked worktrees lexicographically by basename (`1838- < 838-`), so the
141
+ # mis-resolution was deterministic, not a race. The tiers below anchor bare
142
+ # numbers to the issue-number position and make any 2+-way match fatal.
143
+ #
144
+ # Tiers (first tier with any match decides; 2+ matches in a tier is fatal):
145
+ # 1. exact — branch == argument. Guards coexisting feature/838-fix and
146
+ # feature/838-fix-more (both legal via new-feature.sh's title
147
+ # truncation): the full name must resolve, not trip ambiguity.
148
+ # 2. anchored — branch's last path segment == argument; plus, when the
149
+ # argument is all digits, the sequant issue-number branch shapes
150
+ # parseIssueNumberFromBranch (worktree-discovery.ts) recognizes:
151
+ # feature/<N>-*, feature/<N>, issue-<N>, <N>-*.
152
+ # 3. glob — only when the argument itself contains * ? or [ : a plain
153
+ # `case` glob over the branch ref. Keeps the documented, quoted
154
+ # `feature/<issue>-*` form working, now behind the ambiguity gate.
155
+ #
156
+ # bash 3.2 (macOS floor): no arrays/mapfile. Candidates live in a
157
+ # newline-delimited string of "<path>\t<branch>"; each tier re-scans it via
158
+ # process substitution (which keeps loop-set counters in the current shell — a
159
+ # pipe would lose them to a subshell). `while IFS= read -r` preserves whitespace
160
+ # in paths (the #575 hardening).
161
+ _TAB=$(printf '\t')
162
+
163
+ # Collect candidate worktrees: porcelain stanzas carrying a branch ref, minus
164
+ # the main worktree (first entry — without this, `'*'`/`'feature/*'` could
165
+ # resolve to and `git worktree remove` it) and minus the `exec-agent-*`
166
+ # sub-worktree branches parallel isolation leaves behind (real porcelain
167
+ # candidates, per this script's own reaping below).
168
+ CANDIDATES=""
169
+ _wt_path=""
170
+ while IFS= read -r _line; do
171
+ case "$_line" in
172
+ "worktree "*)
173
+ _wt_path="${_line#worktree }"
174
+ ;;
175
+ "branch refs/heads/"*)
176
+ _wt_branch="${_line#branch refs/heads/}"
177
+ if [ "$_wt_path" = "$MAIN_WORKTREE" ]; then
178
+ continue
179
+ fi
180
+ case "$_wt_branch" in
181
+ exec-agent-*) continue ;;
182
+ esac
183
+ CANDIDATES="${CANDIDATES}${_wt_path}${_TAB}${_wt_branch}
184
+ "
185
+ ;;
186
+ esac
187
+ done < <(git worktree list --porcelain)
188
+
189
+ # Classify the argument once: a bare issue number (all digits) enables the
190
+ # anchored issue-number shapes; a glob metacharacter enables the glob tier.
191
+ _is_number=false
192
+ case "$BRANCH_NAME" in
193
+ ''|*[!0-9]*) ;;
194
+ *) _is_number=true ;;
195
+ esac
196
+ _is_glob=false
197
+ case "$BRANCH_NAME" in
198
+ *[*?[]*) _is_glob=true ;;
199
+ esac
200
+
201
+ # Test one branch ref ($2) against one tier ($1); returns 0 on match. Called
202
+ # only as an `if` condition, so a non-zero return never trips `set -e`.
203
+ _branch_matches() {
204
+ case "$1" in
205
+ exact)
206
+ [ "$2" = "$BRANCH_NAME" ]
207
+ ;;
208
+ anchored)
209
+ # Last path segment equals the argument (covers `838-spaced`).
210
+ if [ "${2##*/}" = "$BRANCH_NAME" ]; then
211
+ return 0
212
+ fi
213
+ # All-digit argument: the sequant issue-number branch shapes.
214
+ if [ "$_is_number" = true ]; then
215
+ case "$2" in
216
+ feature/"$BRANCH_NAME"-* | feature/"$BRANCH_NAME" \
217
+ | issue-"$BRANCH_NAME" | "$BRANCH_NAME"-*)
218
+ return 0
219
+ ;;
220
+ esac
221
+ fi
222
+ return 1
223
+ ;;
224
+ glob)
225
+ case "$2" in
226
+ $BRANCH_NAME) return 0 ;;
227
+ esac
228
+ return 1
229
+ ;;
230
+ esac
231
+ }
232
+
233
+ # Walk the tiers in order; the first tier with any match decides. A tier with
234
+ # 2+ matches is fatal (AC-2/AC-3): list every candidate and exit non-zero
235
+ # rather than silently deleting the first.
236
+ WORKTREE_PATH=""
237
+ RESOLVED_BRANCH=""
238
+ for _tier in exact anchored glob; do
239
+ # The glob tier only participates when the argument is itself a glob.
240
+ if [ "$_tier" = glob ] && [ "$_is_glob" != true ]; then
241
+ continue
242
+ fi
243
+
244
+ _match_count=0
245
+ _match_list=""
246
+ while IFS="$_TAB" read -r _cand_path _cand_branch; do
247
+ [ -n "$_cand_branch" ] || continue
248
+ if _branch_matches "$_tier" "$_cand_branch"; then
249
+ _match_count=$((_match_count + 1))
250
+ if [ "$_match_count" -eq 1 ]; then
251
+ WORKTREE_PATH="$_cand_path"
252
+ RESOLVED_BRANCH="$_cand_branch"
253
+ fi
254
+ _match_list="${_match_list} ${_cand_branch}${_TAB}${_cand_path}
255
+ "
256
+ fi
257
+ done < <(printf '%s' "$CANDIDATES")
258
+
259
+ if [ "$_match_count" -gt 1 ]; then
260
+ echo -e "${RED}❌ Error: '$BRANCH_NAME' is ambiguous — it matches ${_match_count} worktree branches:${NC}" >&2
261
+ printf '%s' "$_match_list" >&2
262
+ echo -e "${RED}Refusing to guess. Re-run with the full branch name to disambiguate.${NC}" >&2
263
+ exit 1
264
+ fi
265
+
266
+ if [ "$_match_count" -eq 1 ]; then
267
+ break
268
+ fi
269
+ done
115
270
 
116
271
  if [ -z "$WORKTREE_PATH" ]; then
117
272
  echo -e "${RED}❌ Error: Worktree not found for branch: $BRANCH_NAME${NC}"
@@ -121,12 +276,17 @@ if [ -z "$WORKTREE_PATH" ]; then
121
276
  exit 1
122
277
  fi
123
278
 
124
- echo -e "${BLUE}🧹 Cleaning up worktree for: $BRANCH_NAME${NC}"
279
+ echo -e "${BLUE}🧹 Cleaning up worktree for: $RESOLVED_BRANCH${NC}"
125
280
  echo -e "${BLUE}Path: $WORKTREE_PATH${NC}"
126
281
  echo ""
127
282
 
128
- # Check if PR is merged
129
- PR_STATUS=$(gh pr list --head "$BRANCH_NAME" --state merged --json number,state --jq '.[0].state' 2>/dev/null || echo "")
283
+ # Check if PR is merged. Queries the RESOLVED ref (#838): `--head` is an exact
284
+ # match, so passing the caller's shorthand here reported "not merged" for every
285
+ # invocation form except a full literal branch name — including the
286
+ # `feature/<issue>-*` form the skills and docs prescribe. A warning that always
287
+ # fires carries no signal and makes --yes/--force the mandatory incantation,
288
+ # which re-arms the very bypass reflex #750 removed.
289
+ PR_STATUS=$(gh pr list --head "$RESOLVED_BRANCH" --state merged --json number,state --jq '.[0].state' 2>/dev/null || echo "")
130
290
 
131
291
  # Confirmation gate — only reached when the PR is NOT merged. When MERGED we
132
292
  # short-circuit past this entirely (no prompt, no TTY check) so the documented
@@ -179,9 +339,11 @@ fi
179
339
  echo -e "${BLUE}📂 Removing worktree...${NC}"
180
340
  git worktree remove "$WORKTREE_PATH" --force
181
341
 
182
- # Delete local branch
342
+ # Delete local branch. Uses the RESOLVED ref (#838) — `git branch -D 817` finds
343
+ # no such branch and, being `|| true`-suppressed, failed silently: the script
344
+ # reported "Cleanup complete!" while leaving the branch behind.
183
345
  echo -e "${BLUE}🌿 Deleting local branch...${NC}"
184
- git branch -D "$BRANCH_NAME" 2>/dev/null || true
346
+ git branch -D "$RESOLVED_BRANCH" 2>/dev/null || true
185
347
 
186
348
  # Delete remote branch — hard-gated on merge state. Only delete when the PR is
187
349
  # MERGED or an explicit override flag (--delete-remote/--force) was passed.
@@ -189,7 +351,8 @@ git branch -D "$BRANCH_NAME" 2>/dev/null || true
189
351
  # PR's head branch makes GitHub close the PR unmerged, stranding the work.
190
352
  if [ "$PR_STATUS" = "MERGED" ] || [ "$DELETE_REMOTE" = true ]; then
191
353
  echo -e "${BLUE}☁️ Deleting remote branch...${NC}"
192
- git push origin --delete "$BRANCH_NAME" 2>/dev/null || true
354
+ # Resolved ref (#838): same silent-no-op class as the local delete above.
355
+ git push origin --delete "$RESOLVED_BRANCH" 2>/dev/null || true
193
356
  else
194
357
  echo -e "${YELLOW}⏭️ Skipped remote-branch delete (PR not merged; pass --delete-remote or --force to override).${NC}"
195
358
  fi