sequant 2.1.2 → 2.3.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +95 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/init.d.ts +1 -0
  8. package/dist/src/commands/init.js +118 -0
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +39 -0
  13. package/dist/src/commands/prompt.js +179 -0
  14. package/dist/src/commands/run-display.d.ts +26 -0
  15. package/dist/src/commands/run-display.js +150 -0
  16. package/dist/src/commands/run-progress.d.ts +32 -0
  17. package/dist/src/commands/run-progress.js +76 -0
  18. package/dist/src/commands/run.js +83 -73
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +27 -1
  22. package/dist/src/commands/watch.d.ts +16 -0
  23. package/dist/src/commands/watch.js +147 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  36. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  37. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  38. package/dist/src/lib/locks/index.d.ts +7 -0
  39. package/dist/src/lib/locks/index.js +5 -0
  40. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  41. package/dist/src/lib/locks/lock-manager.js +433 -0
  42. package/dist/src/lib/locks/types.d.ts +59 -0
  43. package/dist/src/lib/locks/types.js +31 -0
  44. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  45. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  46. package/dist/src/lib/relay/activation.d.ts +60 -0
  47. package/dist/src/lib/relay/activation.js +122 -0
  48. package/dist/src/lib/relay/archive.d.ts +34 -0
  49. package/dist/src/lib/relay/archive.js +106 -0
  50. package/dist/src/lib/relay/frame.d.ts +20 -0
  51. package/dist/src/lib/relay/frame.js +76 -0
  52. package/dist/src/lib/relay/index.d.ts +13 -0
  53. package/dist/src/lib/relay/index.js +13 -0
  54. package/dist/src/lib/relay/paths.d.ts +43 -0
  55. package/dist/src/lib/relay/paths.js +59 -0
  56. package/dist/src/lib/relay/pid.d.ts +34 -0
  57. package/dist/src/lib/relay/pid.js +72 -0
  58. package/dist/src/lib/relay/reader.d.ts +35 -0
  59. package/dist/src/lib/relay/reader.js +115 -0
  60. package/dist/src/lib/relay/types.d.ts +68 -0
  61. package/dist/src/lib/relay/types.js +76 -0
  62. package/dist/src/lib/relay/writer.d.ts +48 -0
  63. package/dist/src/lib/relay/writer.js +113 -0
  64. package/dist/src/lib/settings.d.ts +31 -1
  65. package/dist/src/lib/settings.js +18 -3
  66. package/dist/src/lib/skill-version.d.ts +19 -0
  67. package/dist/src/lib/skill-version.js +68 -0
  68. package/dist/src/lib/templates.d.ts +1 -0
  69. package/dist/src/lib/templates.js +1 -1
  70. package/dist/src/lib/version-check.d.ts +60 -5
  71. package/dist/src/lib/version-check.js +97 -9
  72. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  73. package/dist/src/lib/workflow/batch-executor.js +249 -176
  74. package/dist/src/lib/workflow/config-resolver.js +4 -0
  75. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  76. package/dist/src/lib/workflow/heartbeat.js +194 -0
  77. package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
  78. package/dist/src/lib/workflow/phase-executor.js +276 -52
  79. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  80. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  81. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  82. package/dist/src/lib/workflow/platforms/github.js +20 -3
  83. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  84. package/dist/src/lib/workflow/pr-status.js +41 -9
  85. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  86. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  87. package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
  88. package/dist/src/lib/workflow/run-orchestrator.js +382 -29
  89. package/dist/src/lib/workflow/run-reflect.js +1 -1
  90. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  91. package/dist/src/lib/workflow/run-state.js +14 -0
  92. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  93. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  94. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  95. package/dist/src/lib/workflow/state-manager.js +37 -0
  96. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  97. package/dist/src/lib/workflow/state-schema.js +35 -1
  98. package/dist/src/lib/workflow/types.d.ts +74 -1
  99. package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
  100. package/dist/src/lib/workflow/worktree-manager.js +76 -17
  101. package/dist/src/mcp/tools/run.d.ts +44 -0
  102. package/dist/src/mcp/tools/run.js +104 -13
  103. package/dist/src/ui/tui/App.d.ts +14 -0
  104. package/dist/src/ui/tui/App.js +41 -0
  105. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  106. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  107. package/dist/src/ui/tui/Header.d.ts +6 -0
  108. package/dist/src/ui/tui/Header.js +15 -0
  109. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  110. package/dist/src/ui/tui/IssueBox.js +68 -0
  111. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  112. package/dist/src/ui/tui/Spinner.js +18 -0
  113. package/dist/src/ui/tui/index.d.ts +15 -0
  114. package/dist/src/ui/tui/index.js +29 -0
  115. package/dist/src/ui/tui/theme.d.ts +29 -0
  116. package/dist/src/ui/tui/theme.js +52 -0
  117. package/dist/src/ui/tui/truncate.d.ts +11 -0
  118. package/dist/src/ui/tui/truncate.js +31 -0
  119. package/package.json +10 -3
  120. package/templates/agents/sequant-explorer.md +1 -0
  121. package/templates/agents/sequant-qa-checker.md +2 -1
  122. package/templates/agents/sequant-testgen.md +1 -0
  123. package/templates/hooks/post-tool.sh +11 -0
  124. package/templates/hooks/pre-tool.sh +18 -9
  125. package/templates/hooks/relay-check.sh +107 -0
  126. package/templates/relay/frame.txt +11 -0
  127. package/templates/scripts/cleanup-worktree.sh +25 -3
  128. package/templates/scripts/new-feature.sh +6 -0
  129. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  130. package/templates/skills/_shared/references/subagent-types.md +21 -8
  131. package/templates/skills/assess/SKILL.md +261 -94
  132. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  133. package/templates/skills/docs/SKILL.md +141 -22
  134. package/templates/skills/exec/SKILL.md +10 -49
  135. package/templates/skills/fullsolve/SKILL.md +80 -32
  136. package/templates/skills/loop/SKILL.md +28 -0
  137. package/templates/skills/merger/SKILL.md +621 -0
  138. package/templates/skills/qa/SKILL.md +746 -8
  139. package/templates/skills/qa/scripts/quality-checks.sh +47 -1
  140. package/templates/skills/setup/SKILL.md +6 -0
  141. package/templates/skills/spec/SKILL.md +217 -964
  142. package/templates/skills/spec/references/parallel-groups.md +7 -0
  143. package/templates/skills/spec/references/quality-checklist.md +75 -0
  144. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  145. package/templates/skills/test/SKILL.md +0 -27
  146. package/templates/skills/testgen/SKILL.md +24 -44
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: sequant-testgen
3
3
  description: Test stub generator for sequant /testgen phase. Parses verification criteria from /spec comments and generates Jest/Vitest test stubs with Given/When/Then structure. Use when spawned by the /testgen skill.
4
+ # Note: per anthropics/claude-code#43869 this is currently a no-op; agent runs on parent's model
4
5
  model: haiku
5
6
  maxTurns: 25
6
7
  tools:
@@ -13,6 +13,17 @@ if [[ "${CLAUDE_HOOKS_DISABLED:-}" == "true" ]]; then
13
13
  exit 0
14
14
  fi
15
15
 
16
+ # === RELAY CHECK (#383) ===
17
+ # Sourced only when SEQUANT_RELAY=true. The check itself is a single env-var
18
+ # comparison when relay is disabled (default), so non-relay runs incur no cost.
19
+ if [[ "${SEQUANT_RELAY:-}" == "true" ]]; then
20
+ _RELAY_CHECK="$(dirname "${BASH_SOURCE[0]:-$0}")/relay-check.sh"
21
+ if [[ -f "${_RELAY_CHECK}" ]]; then
22
+ # shellcheck source=relay-check.sh disable=SC1091
23
+ source "${_RELAY_CHECK}" || true
24
+ fi
25
+ fi
26
+
16
27
  # === READ INPUT FROM STDIN ===
17
28
  # Claude Code passes tool data as JSON via stdin, not environment variables
18
29
  INPUT_JSON=$(cat)
@@ -104,20 +104,23 @@ if echo "$TOOL_INPUT" | grep -qE '^(env|printenv|export)$'; then
104
104
  fi
105
105
 
106
106
  # Destructive system commands
107
- if echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
107
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
108
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
108
109
  echo "HOOK_BLOCKED: Destructive system command" | tee -a "$HOOK_LOG" >&2
109
110
  exit 2
110
111
  fi
111
112
 
112
113
  # Deployment (should never happen in issue automation)
113
- if echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
114
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
115
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
114
116
  echo "HOOK_BLOCKED: Deployment command" | tee -a "$HOOK_LOG" >&2
115
117
  exit 2
116
118
  fi
117
119
 
118
120
  # Force push
119
121
  # Pattern requires -f to be a standalone flag (not part of branch name like -fix)
120
- if echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
122
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
123
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
121
124
  echo "HOOK_BLOCKED: Force push" | tee -a "$HOOK_LOG" >&2
122
125
  exit 2
123
126
  fi
@@ -127,7 +130,8 @@ fi
127
130
  # - Unpushed commits on main/master
128
131
  # - Uncommitted changes (staged or unstaged)
129
132
  # - Unfinished merge in progress
130
- if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
133
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
134
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
131
135
  CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
132
136
  BLOCK_REASONS=""
133
137
 
@@ -167,7 +171,8 @@ if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
167
171
  fi
168
172
 
169
173
  # CI/CD triggers (automation shouldn't trigger more automation)
170
- if echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
174
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
175
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
171
176
  echo "HOOK_BLOCKED: Workflow trigger" | tee -a "$HOOK_LOG" >&2
172
177
  exit 2
173
178
  fi
@@ -225,7 +230,8 @@ check_sensitive_files() {
225
230
 
226
231
  if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
227
232
  # Security checks for git commit
228
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
233
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
234
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
229
235
  # Skip security checks if --no-verify is used
230
236
  if ! echo "$TOOL_INPUT" | grep -qE -- '--no-verify'; then
231
237
  # Check staged files for secrets
@@ -257,7 +263,8 @@ fi
257
263
  # --- No-Changes Guard (AC-7) ---
258
264
  # Block commits when there are no staged or unstaged changes (prevents empty commits)
259
265
  # Skips for --amend since amending doesn't require new changes
260
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
266
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
267
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
261
268
  if ! echo "$TOOL_INPUT" | grep -qE -- '--amend|--allow-empty'; then
262
269
  # Extract target directory from cd command if present (for worktree commits)
263
270
  # Handles: "cd /path && git commit" or "cd /path; git commit"
@@ -284,7 +291,8 @@ fi
284
291
  # Warn (but don't block) when committing outside a feature worktree
285
292
  # This catches accidental commits to main repo during feature work
286
293
  QUALITY_LOG="${_LOG_DIR}/claude-quality.log"
287
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
294
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
295
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
288
296
  CWD=$(pwd)
289
297
  if ! echo "$CWD" | grep -qE 'worktrees/feature/'; then
290
298
  echo "$(date +%H:%M:%S) WORKTREE_WARNING: Committing outside feature worktree ($CWD)" >> "$QUALITY_LOG"
@@ -295,7 +303,8 @@ fi
295
303
  # --- Commit Message Validation (AC-3) ---
296
304
  # Enforce conventional commits format: type(scope): description
297
305
  # Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf
298
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
306
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
307
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
299
308
  # Extract message from -m flag
300
309
  MSG=""
301
310
 
@@ -0,0 +1,107 @@
1
+ #!/bin/bash
2
+ # Relay check (#383): sourced from post-tool.sh on every PostToolUse when
3
+ # SEQUANT_RELAY=true. Reads unread user messages from inbox.jsonl, renders the
4
+ # framing prompt to stdout (which Claude Code surfaces as additional context),
5
+ # and advances the per-issue read cursor.
6
+ #
7
+ # Fast path (no pending messages): exits silently in well under 5 ms.
8
+ # Slow path (messages pending): renders one framing block per invocation.
9
+
10
+ # Opt-in guard. Fast path #1: env var unset / not exactly "true".
11
+ # Bash precedence makes `cmd1 && cmd2 || cmd3` equivalent to `(cmd1 && cmd2) || cmd3`,
12
+ # so we must use an explicit `if` block to avoid falling through to `exit 0`
13
+ # when the test is FALSE (which is the relay-enabled case).
14
+ if [[ "${SEQUANT_RELAY:-}" != "true" ]]; then
15
+ return 0 2>/dev/null || exit 0
16
+ fi
17
+
18
+ # Resolve relay directory. Inside an isolated worktree, the phase-executor sets
19
+ # SEQUANT_WORKTREE. During the spec phase (main repo) we fall back to the
20
+ # per-issue layout under .sequant/relay/<issue>/.
21
+ if [[ -n "${SEQUANT_WORKTREE:-}" ]]; then
22
+ _RELAY_DIR="${SEQUANT_WORKTREE}/.sequant/relay"
23
+ elif [[ -n "${SEQUANT_ISSUE:-}" ]]; then
24
+ _RELAY_DIR="${PWD}/.sequant/relay/${SEQUANT_ISSUE}"
25
+ else
26
+ # Nothing to do without a target issue or worktree.
27
+ return 0 2>/dev/null || exit 0
28
+ fi
29
+
30
+ _INBOX="${_RELAY_DIR}/inbox.jsonl"
31
+ _CURSOR="${_RELAY_DIR}/.cursor"
32
+
33
+ # Fast path #2: AC-8 — `test -s` is sub-millisecond. Empty/missing inbox skips.
34
+ [[ -s "${_INBOX}" ]] || return 0 2>/dev/null || exit 0
35
+
36
+ # Cursor read (missing → 0). Compare against current inbox line count.
37
+ _CURSOR_VAL=0
38
+ if [[ -f "${_CURSOR}" ]]; then
39
+ _CURSOR_VAL=$(cat "${_CURSOR}" 2>/dev/null || echo 0)
40
+ [[ "${_CURSOR_VAL}" =~ ^[0-9]+$ ]] || _CURSOR_VAL=0
41
+ fi
42
+
43
+ _INBOX_LINES=$(wc -l < "${_INBOX}" 2>/dev/null || echo 0)
44
+ _INBOX_LINES=${_INBOX_LINES// /} # trim whitespace from wc output on macOS
45
+
46
+ # Fast path #3: cursor caught up.
47
+ if [[ "${_INBOX_LINES}" -le "${_CURSOR_VAL}" ]]; then
48
+ return 0 2>/dev/null || exit 0
49
+ fi
50
+
51
+ # Slow path: render frame.
52
+
53
+ # Resolve frame template. Phase-executor sets SEQUANT_RELAY_FRAME to the
54
+ # absolute path within the sequant installation. Falls back to ./templates/
55
+ # (when running from the sequant repo itself).
56
+ _FRAME_PATH="${SEQUANT_RELAY_FRAME:-}"
57
+ if [[ -z "${_FRAME_PATH}" || ! -f "${_FRAME_PATH}" ]]; then
58
+ for _candidate in \
59
+ "${PWD}/.claude/relay/frame.txt" \
60
+ "${PWD}/templates/relay/frame.txt"; do
61
+ if [[ -f "${_candidate}" ]]; then
62
+ _FRAME_PATH="${_candidate}"
63
+ break
64
+ fi
65
+ done
66
+ fi
67
+ if [[ -z "${_FRAME_PATH}" || ! -f "${_FRAME_PATH}" ]]; then
68
+ # Missing template — emit a minimal frame so the user still gets through.
69
+ _FRAME_PATH=""
70
+ fi
71
+
72
+ # Skip the lines we've already shown the model. `tail -n +N` is 1-indexed.
73
+ _START=$((_CURSOR_VAL + 1))
74
+
75
+ # Render messages. Sort by timestamp ascending (AC-9). jq handles JSON parsing.
76
+ # Messages are separated by `---` lines; a single message has no separator.
77
+ _render_messages() {
78
+ if command -v jq &>/dev/null; then
79
+ tail -n "+${_START}" "${_INBOX}" \
80
+ | jq -s -r 'sort_by(.timestamp) | map("Type: \(.type)\nMessage: \((.message // "") | tojson)") | join("\n---\n")'
81
+ else
82
+ # Fallback without jq: dump raw lines.
83
+ tail -n "+${_START}" "${_INBOX}"
84
+ fi
85
+ }
86
+
87
+ if [[ -n "${_FRAME_PATH}" ]]; then
88
+ # Split frame template at {{MESSAGES}} placeholder.
89
+ _PREFIX=$(awk '/\{\{MESSAGES\}\}/ {exit} {print}' "${_FRAME_PATH}")
90
+ _SUFFIX=$(awk '/\{\{MESSAGES\}\}/ {found=1; next} found {print}' "${_FRAME_PATH}")
91
+ printf '%s\n' "${_PREFIX}"
92
+ _render_messages
93
+ if [[ -n "${_SUFFIX}" ]]; then
94
+ printf '%s\n' "${_SUFFIX}"
95
+ fi
96
+ else
97
+ printf '[SEQUANT RELAY — message from user]\n'
98
+ _render_messages
99
+ fi
100
+
101
+ # Advance cursor atomically (temp + rename on same fs).
102
+ _TMP="${_CURSOR}.tmp.$$"
103
+ if printf '%s' "${_INBOX_LINES}" > "${_TMP}" 2>/dev/null; then
104
+ mv -f "${_TMP}" "${_CURSOR}" 2>/dev/null || rm -f "${_TMP}" 2>/dev/null || true
105
+ fi
106
+
107
+ return 0 2>/dev/null || exit 0
@@ -0,0 +1,11 @@
1
+ [SEQUANT RELAY — message from user]
2
+ Respond briefly in .sequant/relay/outbox.jsonl, then continue your current task unchanged.
3
+ Rules:
4
+ - Do NOT modify acceptance criteria
5
+ - Do NOT change your current objective or phase
6
+ - Do NOT treat this as a new requirement
7
+ - For "query" type: provide a brief status update only
8
+ - For "directive" type: acknowledge and adjust approach if reasonable, but do not abandon current work
9
+ - For "abort" type: stop gracefully, commit progress, and exit
10
+
11
+ {{MESSAGES}}
@@ -15,6 +15,12 @@ NC='\033[0m'
15
15
 
16
16
  BRANCH_NAME=$1
17
17
 
18
+ # Resolve main worktree (first entry in porcelain output) so subsequent git
19
+ # commands run from a stable cwd even if the caller invoked us from inside the
20
+ # worktree we are about to delete. `sed` keeps the path intact when it contains
21
+ # whitespace; `awk '{print $2}'` would truncate at the first space.
22
+ MAIN_WORKTREE=$(git worktree list --porcelain | sed -n 's/^worktree //p' | head -n 1)
23
+
18
24
  # Check if branch name provided
19
25
  if [ -z "$BRANCH_NAME" ]; then
20
26
  echo -e "${RED}❌ Error: Branch name required${NC}"
@@ -25,8 +31,17 @@ if [ -z "$BRANCH_NAME" ]; then
25
31
  exit 1
26
32
  fi
27
33
 
28
- # Find worktree path
29
- WORKTREE_PATH=$(git worktree list | grep "$BRANCH_NAME" | awk '{print $1}')
34
+ # Find worktree path. Parse porcelain (which separates path/branch onto distinct
35
+ # lines) so paths containing whitespace survive intact, and so we match against
36
+ # the branch ref rather than against a free-form `grep` over the entire line —
37
+ # the latter false-matches whenever the branch name appears in the path string.
38
+ WORKTREE_PATH=$(git worktree list --porcelain | awk -v target="$BRANCH_NAME" '
39
+ /^worktree / { sub(/^worktree /, ""); path = $0 }
40
+ /^branch refs\/heads\// {
41
+ sub(/^branch refs\/heads\//, "")
42
+ if (index($0, target) > 0) { print path; exit }
43
+ }
44
+ ')
30
45
 
31
46
  if [ -z "$WORKTREE_PATH" ]; then
32
47
  echo -e "${RED}❌ Error: Worktree not found for branch: $BRANCH_NAME${NC}"
@@ -53,6 +68,13 @@ if [ "$PR_STATUS" != "MERGED" ]; then
53
68
  fi
54
69
  fi
55
70
 
71
+ # Move to the main worktree before any destructive operation. If the caller's
72
+ # cwd is inside $WORKTREE_PATH (or any of its .exec-agents/agent-* sub-worktrees
73
+ # below), the first `git worktree remove` would invalidate cwd and every
74
+ # subsequent git/gh call would silently fail with "Unable to read current
75
+ # working directory" — including the ones with `2>/dev/null || true`.
76
+ cd "$MAIN_WORKTREE"
77
+
56
78
  # Clean up any exec-agent sub-worktrees first (from parallel isolation)
57
79
  EXEC_AGENTS_DIR="$WORKTREE_PATH/.exec-agents"
58
80
  if [ -d "$EXEC_AGENTS_DIR" ]; then
@@ -71,7 +93,7 @@ if [ -d "$EXEC_AGENTS_DIR" ]; then
71
93
  rmdir "$EXEC_AGENTS_DIR" 2>/dev/null || true
72
94
  fi
73
95
 
74
- # Remove worktree
96
+ # Remove worktree (cwd already pinned to $MAIN_WORKTREE above)
75
97
  echo -e "${BLUE}📂 Removing worktree...${NC}"
76
98
  git worktree remove "$WORKTREE_PATH" --force
77
99
 
@@ -152,6 +152,12 @@ git pull origin "$BASE_BRANCH"
152
152
  echo -e "${BLUE}🌿 Creating new worktree from ${BASE_BRANCH}...${NC}"
153
153
  git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME"
154
154
 
155
+ # Record the base branch on the new branch so downstream tooling
156
+ # (e.g. phase-executor zero-diff guard, see #537) can resolve the
157
+ # correct comparison ref when the worktree was branched off something
158
+ # other than origin/main.
159
+ git -C "$WORKTREE_DIR" config "branch.${BRANCH_NAME}.sequantBase" "$BASE_BRANCH"
160
+
155
161
  # Navigate to worktree
156
162
  cd "$WORKTREE_DIR"
157
163
 
@@ -0,0 +1,205 @@
1
+ # Behavior-Rule Detection (Shared Heuristic for /spec + /qa)
2
+
3
+ > **Source of truth:** `src/lib/heuristics/behavior-rule-detector.ts`. The
4
+ > keyword set, threshold, and pattern list live in TypeScript so they can be
5
+ > unit-tested (per AC-5 of #552); this doc cites them.
6
+
7
+ ## Why this exists
8
+
9
+ Behavior-rule ACs ("default becomes X", "always include Y", "never skip Z") are
10
+ routinely implemented at **multiple touchpoints** — typically a skill prompt
11
+ (LLM-interpreted) **and** runtime TypeScript that duplicates the same rule.
12
+ Without a shared check, edits land at one site and the other goes stale.
13
+
14
+ **Motivating miss — issue #533** ("default /assess spec phase ON, remove
15
+ bug/docs auto-skip"):
16
+
17
+ - The AC explicitly named `.claude/skills/assess/SKILL.md`.
18
+ - `/spec` scoped the work to that file + CHANGELOG. `/exec` implemented it.
19
+ `/qa` returned `READY_FOR_MERGE`.
20
+ - Meanwhile the runtime CLI (`phase-mapper.ts` `detectPhasesFromLabels` +
21
+ `batch-executor.ts` auto-detect branch) still short-circuited bug/docs
22
+ issues to `exec → qa`, contradicting the new "spec by default" rule.
23
+ - Caught only by manual user follow-up; required two extra commits and four
24
+ doc updates on top of the original PR.
25
+
26
+ A pre-flight grep for `BUG_LABELS` / `DOCS_LABELS` / `"skip spec"` at /spec
27
+ time would have surfaced 90% of these in one pass. That's exactly what this
28
+ heuristic does.
29
+
30
+ ## Trigger keywords
31
+
32
+ A `BEHAVIOR_KEYWORDS` constant in `behavior-rule-detector.ts`. Each keyword
33
+ also matches common inflections via the `KEYWORD_REGEXES` map (mirrors the
34
+ stem-aware pattern landed by #597 in `ac-linter.ts`); word boundaries are
35
+ preserved so `defaultValue` (camelCase identifier) does NOT trigger.
36
+
37
+ | Keyword | Inflections matched | Why it's in the set |
38
+ |---------|---------------------|---------------------|
39
+ | `default` | `defaults`, `defaulted`, `defaulting` | "default becomes X", "defaults to Y" |
40
+ | `always` | (adverb, no inflections) | "always include", "always run" |
41
+ | `never` | (adverb, no inflections) | "never skip", "never run when X" |
42
+ | `rule` | `rules`, `ruled`, `ruling` | "the rule is", "this rule applies" |
43
+ | `behavior` | `behaviors`, `behavioral`, `behaviorally` | "the behavior is", "behaviors change" |
44
+ | `skip` | `skips`, `skipped`, `skipping` | "skip spec", "skipping when X" |
45
+
46
+ ## Trigger threshold (false-positive guard)
47
+
48
+ To avoid flagging localized fixes that happen to mention "default" once
49
+ ("set default value to 5"), the detector requires **either**:
50
+
51
+ 1. **>= 2 distinct keywords** from `BEHAVIOR_KEYWORDS` in the AC description
52
+ (case-insensitive, word-boundary), **OR**
53
+ 2. An **explicit pattern** match (single keyword is enough):
54
+ - Mid-sentence rule constructs:
55
+ - `always X unless Y`
56
+ - `never X unless Y`
57
+ - `default X when Y`
58
+ - Capitalized imperative AC openers (case-sensitive — matches the
59
+ imperative-rule register, not "the system always defaults to..." prose):
60
+ - `Always <verb> ...` — covers AC-5 literal "Always include Y"
61
+ - `Never <verb> ...` — covers AC-5 literal "Never skip Z"
62
+ - `Default <verb> ...` — covers AC-5 literal "Default rule becomes X"
63
+
64
+ Tunable in `EXPLICIT_PATTERNS` in `behavior-rule-detector.ts`.
65
+
66
+ ## Symbol categories surfaced by `findTouchpoints`
67
+
68
+ When the trigger fires, `findTouchpoints` extracts identifier-like substrings
69
+ from the AC and greps the codebase for them, plus any line that contains >= 2
70
+ distinct behavior keywords (catches comment-only sites). Symbol categories:
71
+
72
+ | Category | Example | Why include |
73
+ |----------|---------|-------------|
74
+ | Backticked symbols | `` `BUG_LABELS` ``, `` `phase-mapper.ts` `` | Verbatim from issue body — most specific |
75
+ | `SCREAMING_SNAKE_CASE` constants | `BUG_LABELS`, `DOCS_LABELS`, `SKIP_PHASES` | Dominant runtime-rule pattern |
76
+ | `camelCase` function names | `detectPhasesFromLabels`, `shouldSkipSpec` | Rule-implementing functions |
77
+ | File paths with extensions | `.claude/skills/assess/SKILL.md`, `phase-mapper.ts` | Direct touchpoint citations |
78
+ | Bold spans | `**always X unless Y**` | Author-emphasized rule statements |
79
+
80
+ ## Inverse search (`findSurvivingInverseSymbols`, used by `/qa`)
81
+
82
+ `/qa` runs the detector against the diff blast radius (changed files +
83
+ optional 1-hop importers) using **inverse** keywords derived from each
84
+ asserted keyword. Asymmetric on purpose — an AC asserting the NEW rule
85
+ "always include spec" should look for "skip" / "exclude" / "bypass"
86
+ survivors, not "always" itself.
87
+
88
+ | Asserted keyword | Inverse search terms |
89
+ |------------------|----------------------|
90
+ | `default` | `skip`, `exclude`, `bypass`, `override` |
91
+ | `always` | `skip`, `never`, `exclude`, `conditional`, `shortcut` |
92
+ | `never` | `always`, `default`, `exclude` |
93
+ | `rule` | `exception`, `override`, `shortcut`, `bypass` |
94
+ | `behavior` | `legacy`, `deprecated` |
95
+ | `skip` | `always`, `default`, `exclude` |
96
+
97
+ High-noise common English words (`include`, `run`, `auto`, `old`,
98
+ `previous`) were pruned from this map after QA's self-dogfood pass on #552
99
+ returned 50 survivors entirely from definitional prose. As defense in
100
+ depth, `deriveInverseTerms` also drops any term in the `COMMON_WORDS`
101
+ filter at runtime, so future tunings that re-add a common word are still
102
+ filtered out.
103
+
104
+ A survival inside the diff blast radius -> AC `NOT_MET` with `path:line`
105
+ listed in the QA output (per AC-2 of #552).
106
+
107
+ ## False-positive guards
108
+
109
+ - Test files (`*.test.ts`, `*.spec.ts`) are excluded from `findTouchpoints` —
110
+ they implement *checks* of behavior rules, not the rules themselves.
111
+ - Walk-skip dirs: `node_modules`, `.git`, `dist`, `build`, `.next`,
112
+ `coverage`, `__tests__`, `__snapshots__`.
113
+ - Common English words (`the`, `from`, `should`, `becomes`, ...) are filtered
114
+ from the symbol-extraction pass to avoid grepping for "the".
115
+ - Per-file cap (3) + total cap (200) on `findTouchpoints` results — keeps
116
+ `/spec` output readable when an AC's keywords are ambient in the repo.
117
+ - Total cap (50, `SURVIVOR_TOTAL_CAP`) on `findSurvivingInverseSymbols`
118
+ results — tighter than `findTouchpoints` because survivors are surfaced
119
+ inside the QA verdict and a long list drowns out the rule-relevant hits.
120
+
121
+ ## Performance budget
122
+
123
+ - `detectBehaviorRule(ac)` is a cheap regex pass — runs per AC.
124
+ - `findTouchpoints` and `findSurvivingInverseSymbols` short-circuit to `[]`
125
+ when `detectBehaviorRule` returns `triggered: false`.
126
+ - For `/qa`, the per-AC grep cost is bounded by the diff blast radius (not
127
+ the whole repo). For `/spec`, scope is `src/lib`, `src/commands`, `bin`,
128
+ and `.claude/skills` — `bin/` and `src/commands/` are included because CLI
129
+ option registration (Commander.js `.option()` chains, `RunOptions` interface)
130
+ is a recurring rule-drift site. `templates/skills/` and `skills/` are
131
+ intentionally excluded since they 1:1 mirror `.claude/skills/`.
132
+
133
+ When zero behavior-rule ACs are detected across the issue, both detectors
134
+ should be skipped entirely (no per-file grep pass).
135
+
136
+ ## Where to call from
137
+
138
+ - **`/spec`** — `### Rule Touchpoints (Conditional)` subsection under
139
+ `## Context Gathering`. Calls `findTouchpoints` per AC; emits a
140
+ `## Rule Touchpoints` section in the plan output when any hits found.
141
+ - **`/qa`** — `### 6e. Behavior-Rule Survival Check` between
142
+ `### 6d. Adversarial Re-Read` and `### 7. A+ Status Verdict`. Calls
143
+ `findSurvivingInverseSymbols` per behavior-rule AC; survivals -> AC
144
+ `NOT_MET`, gated through `behavior_rule_survival_status` in section 7.
145
+
146
+ ## API
147
+
148
+ ```typescript
149
+ import {
150
+ detectBehaviorRule,
151
+ findTouchpoints,
152
+ findSurvivingInverseSymbols,
153
+ } from "./src/lib/heuristics/behavior-rule-detector.ts";
154
+
155
+ // Cheap gate
156
+ const detection = detectBehaviorRule(ac);
157
+ // detection.triggered: boolean
158
+ // detection.keywords: BehaviorKeyword[]
159
+ // detection.matchedPattern?: string
160
+
161
+ // /spec: enumerate likely implementation sites
162
+ const hits = findTouchpoints(ac, repoRoot);
163
+ // hits: { path, line, snippet }[]
164
+
165
+ // /qa: search diff blast radius for OLD-rule survivors
166
+ const survivors = findSurvivingInverseSymbols(ac, repoRoot, diffPaths);
167
+ // survivors: { path, line, snippet }[]
168
+ ```
169
+
170
+ ## Known limitations
171
+
172
+ **Meta-recursion** — when an AC describes the detector itself (e.g. lists
173
+ all six trigger keywords as part of explaining the trigger condition),
174
+ `findSurvivingInverseSymbols` will self-fire on the new feature's own
175
+ definitional documentation. The pruned `INVERSE_KEYWORDS` map and
176
+ `SURVIVOR_TOTAL_CAP = 50` mitigate noise, but the heuristic cannot
177
+ distinguish "code that implements the OLD rule" from "documentation that
178
+ defines the rule". Treat survivors inside this reference doc and
179
+ `spec/SKILL.md` / `qa/SKILL.md` as definitional, not stale. See QA's
180
+ self-dogfood result on PR #607 for the original observation.
181
+
182
+ **Inverse-phrase fallback (deferred)** — `findSurvivingInverseSymbols`
183
+ relies on the asserted-→-inverse keyword map. When an AC's inverse rule
184
+ has no obvious symbol or keyword (e.g. "API responses always include the
185
+ user ID field"), the natural next layer is to derive an inverse English
186
+ phrase ("exclude user ID", "don't include user ID") and grep for that.
187
+ Not implemented — file a follow-up issue with the concrete fixture if a
188
+ real `/qa` miss surfaces.
189
+
190
+ ## Tuning
191
+
192
+ Edit `src/lib/heuristics/behavior-rule-detector.ts`:
193
+
194
+ - `BEHAVIOR_KEYWORDS` — keyword stem set (the source of truth)
195
+ - `KEYWORD_REGEXES` — per-stem inflection regex map; word-boundary
196
+ guarded so identifier-shaped tokens (`defaultValue`, `skipperFn`) do
197
+ not trigger
198
+ - `EXPLICIT_PATTERNS` — single-keyword override patterns (mid-sentence
199
+ rule constructs + capitalized imperative AC openers)
200
+ - `INVERSE_KEYWORDS` — asserted -> inverse mapping for `/qa` (common
201
+ English words filtered at runtime in `deriveInverseTerms`)
202
+ - `TOUCHPOINT_ROOTS` — directories scanned by `findTouchpoints`
203
+ - `SURVIVOR_TOTAL_CAP` — total survivor cap on `findSurvivingInverseSymbols`
204
+
205
+ Re-run `npx vitest run src/lib/heuristics` after tuning.
@@ -17,10 +17,15 @@ Claude Code supports exactly **4 built-in subagent types**:
17
17
 
18
18
  Sequant defines **4 custom agents** in `.claude/agents/`. These centralize model, permissions, effort, and tool restrictions that were previously duplicated inline.
19
19
 
20
- | Agent Name | Based On | Model | Permission Mode | Used By |
21
- |------------|----------|-------|-----------------|---------|
20
+ > **Upstream caveat:** `Model` values below are *declared* in the agent files but
21
+ > currently ignored at runtime per anthropics/claude-code#43869 — every subagent
22
+ > inherits the parent session's model. Tiers are kept aligned with intended
23
+ > defaults so they reactivate when upstream fixes ship.
24
+
25
+ | Agent Name | Based On | Model (declared) | Permission Mode | Used By |
26
+ |------------|----------|------------------|-----------------|---------|
22
27
  | `sequant-explorer` | Explore | haiku | (default) | `/spec` |
23
- | `sequant-qa-checker` | general-purpose | haiku | bypassPermissions | `/qa` |
28
+ | `sequant-qa-checker` | general-purpose | sonnet | bypassPermissions | `/qa` |
24
29
  | `sequant-implementer` | general-purpose | (inherits) | bypassPermissions | `/exec` |
25
30
  | `sequant-testgen` | general-purpose | haiku | (default) | `/testgen` |
26
31
 
@@ -68,7 +73,8 @@ Custom agents are defined in `.claude/agents/*.md` with YAML frontmatter:
68
73
  ---
69
74
  name: sequant-qa-checker
70
75
  description: Quality check agent for sequant QA phase.
71
- model: haiku
76
+ # Note: per anthropics/claude-code#43869 this is currently a no-op; agent runs on parent's model
77
+ model: sonnet
72
78
  permissionMode: bypassPermissions
73
79
  effort: low
74
80
  maxTurns: 15
@@ -139,10 +145,17 @@ Agent(subagent_type="Plan",
139
145
 
140
146
  **Default:** Use `haiku` unless the task requires deep reasoning.
141
147
 
148
+ > **Note:** Per anthropics/claude-code#43869, both the per-call `model:`
149
+ > parameter and the agent-definition `model:` field are currently ignored.
150
+ > Subagents inherit the parent session's model regardless of what you specify
151
+ > here. The guidance below reflects the *intended* tier each task should use
152
+ > once upstream fixes land.
153
+
142
154
  Custom agents set their model in the agent definition, so you don't need to specify it inline:
143
155
 
144
156
  ```
145
- # Model comes from .claude/agents/sequant-qa-checker.md (haiku)
157
+ # Model comes from .claude/agents/sequant-qa-checker.md (sonnet, declared)
158
+ # Note: declared tier currently inert per anthropics/claude-code#43869
146
159
  Agent(subagent_type="sequant-qa-checker",
147
160
  prompt="...")
148
161
  ```
@@ -201,10 +214,10 @@ inline when spawning them.
201
214
 
202
215
  | Task | Recommended Agent | Why |
203
216
  |------|-------------------|-----|
204
- | Quality checks (git diff, npm test) | `sequant-qa-checker` | bypassPermissions + haiku + effort:low |
205
- | Codebase exploration | `sequant-explorer` | Read-only, haiku, focused tools |
217
+ | 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) |
206
219
  | Implementation subtask | `sequant-implementer` | Full access, inherits model |
207
- | Test stub generation | `sequant-testgen` | Write access, no Bash, haiku |
220
+ | Test stub generation | `sequant-testgen` | Write access, no Bash (declared model: haiku, inert per #43869) |
208
221
  | One-off custom task | `general-purpose` | Flexible, specify model/mode inline |
209
222
 
210
223
  **CRITICAL:** If your background agent runs `git diff`, `npm test`, `git status`, or any shell command, use `sequant-qa-checker` or `sequant-implementer` (both have bypassPermissions). Do NOT use `general-purpose` without `mode="bypassPermissions"` — Bash calls will silently fail.