sequant 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -5
- package/dist/bin/cli.js +143 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- package/dist/src/commands/logs.js +14 -5
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +31 -3
- package/dist/src/commands/ready.js +53 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +38 -4
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -0
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +87 -0
- package/dist/src/lib/settings.js +106 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +552 -60
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -52
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -20
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/types.d.ts +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/release/SKILL.md +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- package/dist/src/lib/workflow/run-summary.js +0 -142
- 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)
|
|
@@ -60,7 +72,23 @@ mkdir -p "$_LOG_DIR" 2>/dev/null || _LOG_DIR="${_TMPDIR}"
|
|
|
60
72
|
|
|
61
73
|
TIMING_LOG="${_LOG_DIR}/claude-timing.log"
|
|
62
74
|
HOOK_LOG="${_LOG_DIR}/claude-hook.log"
|
|
63
|
-
|
|
75
|
+
|
|
76
|
+
# Parallel-group marker naming (#881). The scheme lives in one sourced helper
|
|
77
|
+
# next to this hook so the writer (/exec skill) and the readers (this hook and
|
|
78
|
+
# post-tool.sh) cannot drift. PARALLEL_MARKER_PREFIX is project-scoped, so a
|
|
79
|
+
# parallel group in another project no longer collides in the shared temp dir.
|
|
80
|
+
_MARKER_HELPER="$(dirname "${BASH_SOURCE[0]:-$0}")/parallel-marker.sh"
|
|
81
|
+
if [[ -f "$_MARKER_HELPER" ]]; then
|
|
82
|
+
# shellcheck source=parallel-marker.sh disable=SC1091
|
|
83
|
+
source "$_MARKER_HELPER"
|
|
84
|
+
PARALLEL_MARKER_PREFIX="$(parallel_marker_prefix)"
|
|
85
|
+
PARALLEL_MARKER_PROJECT_ROOT="$(parallel_marker_project_root)"
|
|
86
|
+
else
|
|
87
|
+
# Helper missing (unexpected): fall back to the pre-#881 global prefix so the
|
|
88
|
+
# hook still functions, accepting the old cross-project collision risk.
|
|
89
|
+
PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
|
|
90
|
+
PARALLEL_MARKER_PROJECT_ROOT=""
|
|
91
|
+
fi
|
|
64
92
|
|
|
65
93
|
# === HELPERS ===
|
|
66
94
|
|
|
@@ -230,6 +258,14 @@ seg_match() {
|
|
|
230
258
|
[[ -n "$SEGMENTS" ]] && grep -qE "$1" <<< "$SEGMENTS"
|
|
231
259
|
}
|
|
232
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
|
+
|
|
233
269
|
# Precompute the segment list once, for Bash commands only.
|
|
234
270
|
SEGMENTS=""
|
|
235
271
|
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
@@ -237,17 +273,20 @@ if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
|
237
273
|
fi
|
|
238
274
|
|
|
239
275
|
# === AGENT ID DETECTION ===
|
|
240
|
-
# For parallel agents, detect group ID from marker files
|
|
241
|
-
#
|
|
276
|
+
# For parallel agents, detect group ID from marker files. The glob is scoped to
|
|
277
|
+
# the current project's marker prefix (#881) so a foreign project's marker never
|
|
278
|
+
# labels this session's timing rows.
|
|
242
279
|
AGENT_ID=""
|
|
280
|
+
_MARKER_BASE=$(basename "$PARALLEL_MARKER_PREFIX")
|
|
243
281
|
# Find marker files using find (works in both bash and zsh)
|
|
244
282
|
while IFS= read -r marker; do
|
|
245
283
|
if [[ -n "$marker" && -f "$marker" ]]; then
|
|
246
|
-
# Extract group ID
|
|
247
|
-
AGENT_ID=$(basename "$marker"
|
|
284
|
+
# Extract group ID: strip the project-scoped prefix and the suffix.
|
|
285
|
+
AGENT_ID=$(basename "$marker" .marker)
|
|
286
|
+
AGENT_ID=${AGENT_ID#"$_MARKER_BASE"}
|
|
248
287
|
break
|
|
249
288
|
fi
|
|
250
|
-
done < <(find "${_TMPDIR}" -maxdepth 1 -name "
|
|
289
|
+
done < <(find "${_TMPDIR}" -maxdepth 1 -name "${_MARKER_BASE}*.marker" 2>/dev/null)
|
|
251
290
|
|
|
252
291
|
# === TIMING START ===
|
|
253
292
|
# Include agent ID in log format if available (AC-4)
|
|
@@ -439,6 +478,214 @@ if seg_match 'git reset.*(--hard|origin)'; then
|
|
|
439
478
|
fi
|
|
440
479
|
fi
|
|
441
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
|
+
|
|
442
689
|
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
443
690
|
if seg_match 'gh workflow run'; then
|
|
444
691
|
log_block "workflow-trigger"
|
|
@@ -617,6 +864,36 @@ if [[ "$TOOL_NAME" == "Bash" ]] && seg_match 'git commit'; then
|
|
|
617
864
|
fi
|
|
618
865
|
fi
|
|
619
866
|
|
|
867
|
+
# Resolve a path to its canonical form, tolerating components that do not exist
|
|
868
|
+
# yet. Plain `realpath` fails outright on a missing path, and BSD/macOS realpath
|
|
869
|
+
# has no `-m`. The old code fell back to the raw string on failure, which broke
|
|
870
|
+
# the comparison below whenever the worktree path crossed a symlink: a Write
|
|
871
|
+
# creating a NEW file resolved to nothing (raw "/tmp/.../x.ts") while the
|
|
872
|
+
# existing worktree dir resolved through the symlink ("/private/tmp/.../"), so
|
|
873
|
+
# the prefix test failed and writes *inside* the worktree were blocked. macOS
|
|
874
|
+
# hits this every time, since /tmp is a symlink to /private/tmp.
|
|
875
|
+
#
|
|
876
|
+
# Walk up to the nearest existing ancestor, canonicalise that, then re-append
|
|
877
|
+
# the missing tail so both sides normalise identically.
|
|
878
|
+
resolve_path_allow_missing() {
|
|
879
|
+
local p="$1"
|
|
880
|
+
local suffix=""
|
|
881
|
+
|
|
882
|
+
while [[ ! -e "$p" && "$p" != "/" && "$p" != "." && -n "$p" ]]; do
|
|
883
|
+
suffix="/$(basename "$p")$suffix"
|
|
884
|
+
local parent
|
|
885
|
+
parent="$(dirname "$p")"
|
|
886
|
+
[[ "$parent" == "$p" ]] && break
|
|
887
|
+
p="$parent"
|
|
888
|
+
done
|
|
889
|
+
|
|
890
|
+
local resolved
|
|
891
|
+
resolved="$(realpath "$p" 2>/dev/null || echo "$p")"
|
|
892
|
+
# Avoid a doubled slash when the resolved ancestor is "/"
|
|
893
|
+
[[ "$resolved" == "/" ]] && resolved=""
|
|
894
|
+
echo "${resolved}${suffix}"
|
|
895
|
+
}
|
|
896
|
+
|
|
620
897
|
# === WORKTREE PATH ENFORCEMENT ===
|
|
621
898
|
# Enforces that file operations stay within the designated worktree
|
|
622
899
|
# Sources for worktree path (in priority order):
|
|
@@ -631,10 +908,21 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
|
631
908
|
EXPECTED_WORKTREE="$SEQUANT_WORKTREE"
|
|
632
909
|
fi
|
|
633
910
|
|
|
634
|
-
# Priority 2: Fall back to parallel marker file
|
|
911
|
+
# Priority 2: Fall back to parallel marker file. The glob prefix is already
|
|
912
|
+
# scoped to this project's hash (#881, AC-2), so a foreign project's marker
|
|
913
|
+
# is not matched here. As defense-in-depth (AC-3), also verify the marker's
|
|
914
|
+
# stored owning project root (line 2) matches the current one before trusting
|
|
915
|
+
# it — a stale or hand-placed marker with a colliding name is ignored rather
|
|
916
|
+
# than allowed to redirect enforcement.
|
|
635
917
|
if [[ -z "$EXPECTED_WORKTREE" ]]; then
|
|
636
918
|
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
637
919
|
if [[ -f "$marker" ]]; then
|
|
920
|
+
MARKER_PROJECT_ROOT=$(sed -n '2p' "$marker" 2>/dev/null || true)
|
|
921
|
+
if [[ -n "$MARKER_PROJECT_ROOT" && -n "$PARALLEL_MARKER_PROJECT_ROOT" \
|
|
922
|
+
&& "$MARKER_PROJECT_ROOT" != "$PARALLEL_MARKER_PROJECT_ROOT" ]]; then
|
|
923
|
+
# Foreign owner — ignore this marker entirely.
|
|
924
|
+
continue
|
|
925
|
+
fi
|
|
638
926
|
# Read expected worktree path from marker file (first line)
|
|
639
927
|
EXPECTED_WORKTREE=$(head -1 "$marker" 2>/dev/null || true)
|
|
640
928
|
break
|
|
@@ -660,9 +948,14 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
|
660
948
|
fi
|
|
661
949
|
|
|
662
950
|
if [[ -n "$FILE_PATH" ]]; then
|
|
663
|
-
# Resolve to absolute
|
|
664
|
-
|
|
665
|
-
|
|
951
|
+
# Resolve to canonical absolute paths for consistent comparison.
|
|
952
|
+
# resolve_path_allow_missing (defined above) tolerates a file that
|
|
953
|
+
# does not exist yet; a bare `realpath` fallback-to-raw-string broke
|
|
954
|
+
# new-file Writes whenever the worktree path crossed a symlink
|
|
955
|
+
# (macOS /tmp -> /private/tmp): the new file stayed raw while the
|
|
956
|
+
# existing worktree dir resolved, so the prefix test always failed.
|
|
957
|
+
REAL_FILE_PATH=$(resolve_path_allow_missing "$FILE_PATH")
|
|
958
|
+
REAL_WORKTREE=$(resolve_path_allow_missing "$EXPECTED_WORKTREE")
|
|
666
959
|
|
|
667
960
|
# Check if file path is within the expected worktree
|
|
668
961
|
if [[ "$REAL_FILE_PATH" != "$REAL_WORKTREE"* ]]; then
|