diff --git a/.agent/STALLED-WORKTREE-RECOVERY.md b/.agent/STALLED-WORKTREE-RECOVERY.md index 5d970a3..6ed4557 100644 --- a/.agent/STALLED-WORKTREE-RECOVERY.md +++ b/.agent/STALLED-WORKTREE-RECOVERY.md @@ -4,6 +4,8 @@ The Guardex Codex launcher auto-finishes a branch only when the codex CLI exits `scripts/agent-stalled-report.sh` is a quiet wrapper around `scripts/agent-autofinish-watch.sh --once --dry-run` that surfaces stalled worktrees. It is wired as a `SessionStart` hook in `.claude/settings.json`, so each Claude Code session begins with a one-line summary per stalled branch (and is silent when nothing is stalled). +`scripts/agent-claude-stop-finish.sh` is wired as a Claude `Stop` hook. When Claude stops inside an `agent/*` worktree and there is committed or dirty work, it delegates to `gx branch finish --branch --base --via-pr --wait-for-merge --cleanup`. Set `GUARDEX_CLAUDE_STOP_FINISH=clean` to only finish already-committed lanes, or `GUARDEX_CLAUDE_STOP_FINISH=off` to disable the stop-time handoff. + To act on the report: - Inspect: `bash scripts/agent-autofinish-watch.sh --once --dry-run` diff --git a/.claude/settings.json b/.claude/settings.json index 3917961..4ccc7d3 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -61,6 +61,16 @@ } ] } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/scripts/agent-claude-stop-finish.sh\"" + } + ] + } ] } } diff --git a/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/.openspec.yaml b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/.openspec.yaml new file mode 100644 index 0000000..d6b53de --- /dev/null +++ b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-30 diff --git a/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/proposal.md b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/proposal.md new file mode 100644 index 0000000..3762a40 --- /dev/null +++ b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/proposal.md @@ -0,0 +1,16 @@ +## Why + +- Claude Code can start Guardex agent worktrees correctly, but completed lanes can remain stranded when Claude stops without running the final `gx branch finish --via-pr --wait-for-merge --cleanup` command. +- A stranded lane keeps work off `main`, leaves the sandbox around, and can keep locks or follow-up context dangling. + +## What Changes + +- Add a managed Claude `Stop` hook that runs only from an `agent/*` worktree and delegates to the canonical Guardex finish flow. +- Ship the hook as a managed `templates/scripts` helper and wire it through `gx setup` and `gx claude install`. +- Keep the helper fail-open: when the finish flow cannot complete, print the retry command and keep the sandbox instead of blocking Claude shutdown. + +## Impact + +- Affects Claude Code integration settings, managed script scaffolding, and worktree finish recovery. +- Default behavior auto-commits dirty agent lanes through the existing finish flow; operators can set `GUARDEX_CLAUDE_STOP_FINISH=clean` or `off` to reduce or disable automation. +- The helper never runs on protected base branches or recursive Stop-hook invocations. diff --git a/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/specs/make-claude-worktree-lanes-land-automatically/spec.md b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/specs/make-claude-worktree-lanes-land-automatically/spec.md new file mode 100644 index 0000000..5737398 --- /dev/null +++ b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/specs/make-claude-worktree-lanes-land-automatically/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Claude Stop hook lands agent worktrees +Guardex SHALL install a Claude `Stop` hook that attempts to finish the current lane when Claude stops inside an `agent/*` worktree with committed or dirty work. + +#### Scenario: Committed agent worktree is handed to finish flow +- **GIVEN** Claude stops with `cwd` inside an `agent/*` worktree +- **AND** the worktree has commits ahead of its resolved base branch +- **WHEN** the `Stop` hook runs +- **THEN** Guardex SHALL invoke `gx branch finish --branch --base --via-pr --wait-for-merge --cleanup`. + +#### Scenario: Dirty worktree follows configured mode +- **GIVEN** Claude stops with `cwd` inside an `agent/*` worktree that has uncommitted changes +- **WHEN** `GUARDEX_CLAUDE_STOP_FINISH=clean` +- **THEN** Guardex SHALL leave the sandbox open and print the exact finish command. +- **WHEN** `GUARDEX_CLAUDE_STOP_FINISH` is unset or set to commit mode +- **THEN** Guardex SHALL delegate to the existing finish flow, which owns auto-commit, PR, merge wait, and cleanup. + +#### Scenario: Stop hook stays fail-open +- **WHEN** the hook runs outside an `agent/*` worktree, during a recursive Stop hook invocation, or with no local work to finish +- **THEN** Guardex SHALL exit successfully without invoking the finish flow. +- **WHEN** the finish flow fails +- **THEN** Guardex SHALL keep the sandbox and print the retry command without failing Claude's Stop hook. diff --git a/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/tasks.md b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/tasks.md new file mode 100644 index 0000000..3b0db4d --- /dev/null +++ b/openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/tasks.md @@ -0,0 +1,34 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58`; branch=`agent/codex/make-claude-worktree-lanes-land-automati-2026-06-30-11-58`; scope=`Claude Stop hook lands agent worktrees via canonical Guardex finish flow`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58` on branch `agent/codex/make-claude-worktree-lanes-land-automati-2026-06-30-11-58`. Work inside the existing sandbox, review `openspec/changes/agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/make-claude-worktree-lanes-land-automati-2026-06-30-11-58 --base main --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58`. +- [x] 1.2 Define normative requirements in `specs/make-claude-worktree-lanes-land-automatically/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. +- [x] 3.2 Run `openspec validate agent-codex-make-claude-worktree-lanes-land-automati-2026-06-30-11-58 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/make-claude-worktree-lanes-land-automati-2026-06-30-11-58 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/scripts/agent-claude-stop-finish.sh b/scripts/agent-claude-stop-finish.sh new file mode 120000 index 0000000..ba18513 --- /dev/null +++ b/scripts/agent-claude-stop-finish.sh @@ -0,0 +1 @@ +../templates/scripts/agent-claude-stop-finish.sh \ No newline at end of file diff --git a/scripts/check-script-symlinks.sh b/scripts/check-script-symlinks.sh index 62b9df8..c568590 100755 --- a/scripts/check-script-symlinks.sh +++ b/scripts/check-script-symlinks.sh @@ -18,6 +18,7 @@ required_symlinks=( scripts/agent-branch-start.sh scripts/agent-branch-finish.sh scripts/agent-branch-merge.sh + scripts/agent-claude-stop-finish.sh scripts/agent-file-locks.py scripts/agent-preflight.sh scripts/agent-stalled-report.sh diff --git a/src/cli/commands/claude.js b/src/cli/commands/claude.js index a1c09ab..94bf49f 100644 --- a/src/cli/commands/claude.js +++ b/src/cli/commands/claude.js @@ -52,6 +52,7 @@ const EXPECTED_HOOK_MATCHERS = { UserPromptSubmit: ['skill_activation.py', 'agent_branch_advisor.py'], PreToolUse: ['skill_guard.py'], PostToolUse: ['post_edit_tracker.py', 'skill_tracker.py'], + Stop: ['agent-claude-stop-finish.sh'], }; const TEMPLATE_DEFAULT_SETTINGS = { @@ -115,6 +116,16 @@ const TEMPLATE_DEFAULT_SETTINGS = { ], }, ], + Stop: [ + { + hooks: [ + { + type: 'command', + command: 'bash "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}/scripts/agent-claude-stop-finish.sh"', + }, + ], + }, + ], }, }; diff --git a/src/context.js b/src/context.js index 5fcf6f8..a5c945d 100644 --- a/src/context.js +++ b/src/context.js @@ -143,7 +143,7 @@ function toDestinationPath(relativeTemplatePath) { // scripts/ ↔ templates/scripts/ layout convention (single source of truth): // -// 1. PAIRED files (10): tracked on both sides; scripts/ is a symlink +// 1. PAIRED files: tracked on both sides; scripts/ is a symlink // to ../templates/scripts/ per PR #548. See // scripts/check-script-symlinks.sh for the exact list. CI + the // .githooks/pre-commit shim both enforce that no symlink is ever @@ -165,6 +165,7 @@ const TEMPLATE_FILES = [ 'scripts/agent-preflight.sh', 'scripts/agent-stalled-report.sh', 'scripts/agent-autofinish-watch.sh', + 'scripts/agent-claude-stop-finish.sh', 'scripts/guardex-docker-loader.sh', 'scripts/guardex-env.sh', 'github/pull.yml.example', diff --git a/templates/scripts/agent-claude-stop-finish.sh b/templates/scripts/agent-claude-stop-finish.sh new file mode 100755 index 0000000..3fbd531 --- /dev/null +++ b/templates/scripts/agent-claude-stop-finish.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Claude Stop hook: when a Claude session ends inside an agent/* worktree, +# hand the lane to the canonical Guardex finish flow so commits, PR merge wait, +# and sandbox cleanup stay coupled. +# +# Modes: +# GUARDEX_CLAUDE_STOP_FINISH=commit auto-commit dirty work via gx finish (default) +# GUARDEX_CLAUDE_STOP_FINISH=clean finish only clean committed lanes +# GUARDEX_CLAUDE_STOP_FINISH=off disable the hook +# +# The hook is fail-open: finish failures keep the sandbox and print a recovery +# command, but never block Claude's Stop event. + +set -euo pipefail + +NODE_BIN="${GUARDEX_NODE_BIN:-node}" +CLI_ENTRY="${GUARDEX_CLI_ENTRY:-}" + +run_guardex_cli() { + if [[ -n "$CLI_ENTRY" ]]; then + "$NODE_BIN" "$CLI_ENTRY" "$@" + return $? + fi + if command -v gx >/dev/null 2>&1; then + gx "$@" + return $? + fi + if command -v gitguardex >/dev/null 2>&1; then + gitguardex "$@" + return $? + fi + echo "[agent-claude-stop-finish] Guardex CLI entrypoint unavailable; rerun via gx." >&2 + return 127 +} + +normalize_mode() { + local raw="${1:-commit}" lowered + lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" + case "$lowered" in + ''|1|true|yes|on|commit|dirty|auto) printf 'commit' ;; + clean|committed) printf 'clean' ;; + 0|false|no|off|none|disabled) printf 'off' ;; + *) + echo "[agent-claude-stop-finish] Unknown GUARDEX_CLAUDE_STOP_FINISH='${raw}', using commit." >&2 + printf 'commit' + ;; + esac +} + +json_field() { + local key="$1" + python3 -c ' +import json +import sys + +key = sys.argv[1] +try: + data = json.loads(sys.stdin.read() or "{}") +except Exception: + data = {} +value = data.get(key, "") +if isinstance(value, bool): + print("true" if value else "false") +elif value is not None: + print(value) +' "$key" 2>/dev/null || true +} + +resolve_base_branch() { + local repo="$1" branch="$2" configured head_ref cand + configured="$(git -C "$repo" config --get "branch.${branch}.guardexBase" 2>/dev/null || true)" + if [[ -n "$configured" ]]; then + printf '%s' "$configured" + return 0 + fi + configured="$(git -C "$repo" config --get multiagent.baseBranch 2>/dev/null || true)" + if [[ -n "$configured" ]]; then + printf '%s' "$configured" + return 0 + fi + head_ref="$(git -C "$repo" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)" + if [[ -n "$head_ref" ]]; then + printf '%s' "${head_ref#origin/}" + return 0 + fi + for cand in main dev master; do + if git -C "$repo" show-ref --verify --quiet "refs/heads/${cand}"; then + printf '%s' "$cand" + return 0 + fi + done + printf 'main' +} + +base_ref_for_count() { + local repo="$1" base="$2" + if git -C "$repo" show-ref --verify --quiet "refs/heads/${base}"; then + printf '%s' "$base" + return 0 + fi + if git -C "$repo" show-ref --verify --quiet "refs/remotes/origin/${base}"; then + printf 'origin/%s' "$base" + return 0 + fi + return 1 +} + +dirty_count() { + local wt="$1" + git -C "$wt" status --porcelain -- . ":(exclude).omx/state/agent-file-locks.json" 2>/dev/null | grep -c . || true +} + +payload="$(cat || true)" +event="$(printf '%s' "$payload" | json_field hook_event_name)" +stop_hook_active="$(printf '%s' "$payload" | json_field stop_hook_active)" +session_cwd="$(printf '%s' "$payload" | json_field cwd)" + +if [[ -n "$event" && "$event" != "Stop" ]]; then + exit 0 +fi +if [[ "$stop_hook_active" == "true" ]]; then + exit 0 +fi + +mode="$(normalize_mode "${GUARDEX_CLAUDE_STOP_FINISH:-commit}")" +if [[ "$mode" == "off" ]]; then + exit 0 +fi + +if [[ -z "$session_cwd" || ! -d "$session_cwd" ]]; then + session_cwd="$PWD" +fi + +if ! worktree_path="$(git -C "$session_cwd" rev-parse --show-toplevel 2>/dev/null)"; then + exit 0 +fi +if ! branch="$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null)"; then + exit 0 +fi +if [[ "$branch" != agent/* ]]; then + exit 0 +fi + +base_branch="$(resolve_base_branch "$worktree_path" "$branch")" +base_ref="" +ahead="0" +if base_ref="$(base_ref_for_count "$worktree_path" "$base_branch")"; then + ahead="$(git -C "$worktree_path" rev-list --count "${base_ref}..${branch}" 2>/dev/null || printf '0')" +fi +dirty="$(dirty_count "$worktree_path")" + +if [[ "$dirty" -eq 0 && "$ahead" -eq 0 ]]; then + exit 0 +fi + +finish_cmd=(branch finish --branch "$branch" --base "$base_branch" --via-pr --wait-for-merge --cleanup) + +if [[ "$dirty" -gt 0 && "$mode" == "clean" ]]; then + echo "[agent-claude-stop-finish] ${branch}: ${dirty} uncommitted change(s); clean-only mode left the sandbox open." >&2 + echo "[agent-claude-stop-finish] Finish manually when ready: gx ${finish_cmd[*]}" >&2 + exit 0 +fi + +echo "[agent-claude-stop-finish] ${branch}: handing off to gx ${finish_cmd[*]}" >&2 +finish_output="" +if finish_output="$(run_guardex_cli "${finish_cmd[@]}" 2>&1)"; then + printf '%s\n' "$finish_output" + echo "[agent-claude-stop-finish] ${branch}: finish completed." >&2 + exit 0 +fi + +printf '%s\n' "$finish_output" >&2 +echo "[agent-claude-stop-finish] ${branch}: finish did not complete; sandbox kept at ${worktree_path}." >&2 +echo "[agent-claude-stop-finish] Retry when ready: gx ${finish_cmd[*]}" >&2 +exit 0 diff --git a/test/agent-claude-stop-finish.test.js b/test/agent-claude-stop-finish.test.js new file mode 100644 index 0000000..aadf627 --- /dev/null +++ b/test/agent-claude-stop-finish.test.js @@ -0,0 +1,111 @@ +// Claude Stop hook coverage. The hook must delegate completed agent worktrees +// to `gx branch finish --via-pr --wait-for-merge --cleanup` without firing on +// recursive Stop-hook invocations or clean-only dirty lanes. + +const { + test, + assert, + fs, + os, + path, + cp, + initRepo, + seedCommit, + runHumanCmd, + createFakeBin, + defineSpawnSuite, +} = require('./helpers/install-test-helpers'); + +const HOOK = path.resolve(__dirname, '..', 'scripts', 'agent-claude-stop-finish.sh'); + +function makeLane(repoDir, branch, { commitAhead = false, dirty = false } = {}) { + const wt = path.join(repoDir, '.omc', 'agent-worktrees', branch.replace(/\//g, '__')); + const add = runHumanCmd('git', ['worktree', 'add', '-b', branch, wt, 'main'], repoDir); + assert.equal(add.status, 0, add.stderr || add.stdout); + const configBase = runHumanCmd('git', ['config', `branch.${branch}.guardexBase`, 'main'], repoDir); + assert.equal(configBase.status, 0, configBase.stderr || configBase.stdout); + if (commitAhead) { + fs.writeFileSync(path.join(wt, 'work.txt'), 'change\n'); + assert.equal(runHumanCmd('git', ['add', '-A'], wt).status, 0); + assert.equal(runHumanCmd('git', ['commit', '-m', 'lane work'], wt).status, 0); + } + if (dirty) { + fs.writeFileSync(path.join(wt, 'dirty.txt'), 'uncommitted\n'); + } + return wt; +} + +function fakeGx(marker) { + return createFakeBin('gx', `printf '%s\\n' "$@" > "${marker}"`); +} + +function invokeHook(worktree, extraEnv = {}, payload = {}) { + return cp.spawnSync('bash', [HOOK], { + cwd: worktree, + input: JSON.stringify({ + hook_event_name: 'Stop', + cwd: worktree, + session_id: 'test-claude-stop-finish', + ...payload, + }), + encoding: 'utf8', + env: { ...process.env, ...extraEnv }, + }); +} + +defineSpawnSuite('agent-claude-stop-finish', () => { + test('delegates a committed agent lane to gx branch finish', () => { + const repoDir = initRepo({ branch: 'main' }); + seedCommit(repoDir); + const wt = makeLane(repoDir, 'agent/test/done', { commitAhead: true }); + const marker = path.join(os.tmpdir(), `gx-stop-finish-${process.pid}-${Date.now()}`); + const fake = fakeGx(marker); + + const res = invokeHook(wt, { PATH: `${fake.fakeBin}:${process.env.PATH}` }); + assert.equal(res.status, 0, res.stderr || res.stdout); + assert.match(res.stderr, /agent\/test\/done: handing off to gx branch finish/); + assert.deepEqual(fs.readFileSync(marker, 'utf8').trim().split('\n'), [ + 'branch', + 'finish', + '--branch', + 'agent/test/done', + '--base', + 'main', + '--via-pr', + '--wait-for-merge', + '--cleanup', + ]); + }); + + test('clean-only mode does not auto-commit dirty worktrees', () => { + const repoDir = initRepo({ branch: 'main' }); + seedCommit(repoDir); + const wt = makeLane(repoDir, 'agent/test/dirty', { dirty: true }); + const marker = path.join(os.tmpdir(), `gx-stop-clean-${process.pid}-${Date.now()}`); + const fake = fakeGx(marker); + + const res = invokeHook(wt, { + PATH: `${fake.fakeBin}:${process.env.PATH}`, + GUARDEX_CLAUDE_STOP_FINISH: 'clean', + }); + assert.equal(res.status, 0, res.stderr || res.stdout); + assert.match(res.stderr, /clean-only mode left the sandbox open/); + assert.equal(fs.existsSync(marker), false, 'gx must not run for dirty clean-only lanes'); + }); + + test('recursive Stop hook invocations are ignored', () => { + const repoDir = initRepo({ branch: 'main' }); + seedCommit(repoDir); + const wt = makeLane(repoDir, 'agent/test/recursive', { commitAhead: true }); + const marker = path.join(os.tmpdir(), `gx-stop-recursive-${process.pid}-${Date.now()}`); + const fake = fakeGx(marker); + + const res = invokeHook( + wt, + { PATH: `${fake.fakeBin}:${process.env.PATH}` }, + { stop_hook_active: true }, + ); + assert.equal(res.status, 0, res.stderr || res.stdout); + assert.equal(fs.existsSync(marker), false, 'gx must not run while Stop hook is already active'); + }); +}); diff --git a/test/claude-install.test.js b/test/claude-install.test.js index ae2248a..bc7da6d 100644 --- a/test/claude-install.test.js +++ b/test/claude-install.test.js @@ -12,7 +12,7 @@ const claudeModule = require('../src/cli/commands/claude'); function makeRepo() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gx-claude-')); - const run = (...args) => cp.spawnSync('git', args, { cwd: dir, encoding: 'utf8' }); + const run = (...args) => cp.spawnSync('git', ['-c', 'core.hooksPath=/dev/null', ...args], { cwd: dir, encoding: 'utf8' }); assert.equal(run('init', '-q', '-b', 'main').status, 0); assert.equal(run('config', 'user.email', 'test@example.com').status, 0); assert.equal(run('config', 'user.name', 'Test').status, 0); @@ -276,3 +276,14 @@ test('branch advisor is wired into SessionStart and UserPromptSubmit', () => { (g.hooks || []).map((h) => h.command || '')); assert.ok(sessionCmds.some((cmd) => cmd.includes('agent-stalled-report.sh')), 'stalled report preserved'); }); + +test('Claude Stop hook is wired to finish agent worktrees', () => { + const merged = claudeModule.mergeSettings(null, claudeModule.TEMPLATE_DEFAULT_SETTINGS); + const commands = (merged.hooks.Stop || []).flatMap((g) => + (g.hooks || []).map((h) => h.command || '')); + assert.ok( + commands.some((cmd) => cmd.includes('agent-claude-stop-finish.sh')), + 'Stop should invoke agent-claude-stop-finish.sh', + ); + assert.deepEqual(claudeModule.EXPECTED_HOOK_MATCHERS.Stop, ['agent-claude-stop-finish.sh']); +}); diff --git a/test/setup.test.js b/test/setup.test.js index a15b397..6f4113f 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -86,6 +86,7 @@ test('setup provisions workflow files and repo config', () => { 'scripts/guardex-env.sh', 'scripts/agent-stalled-report.sh', 'scripts/agent-autofinish-watch.sh', + 'scripts/agent-claude-stop-finish.sh', '.githooks/pre-commit', '.githooks/pre-push', '.githooks/post-merge',