Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.5.5",
"version": "6.5.6",
"source": "./",
"author": {
"name": "Jon Langevin",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents: design, review, planning, execution, monitoring, and retrospectives",
"version": "6.5.5",
"version": "6.5.6",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "autodev",
"displayName": "Autonomous Dev Kit",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.5.5",
"version": "6.5.6",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/hooks-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ jobs:
run: bash tests/hook-stdout-discipline.sh
- name: Hook contract tests
run: bash tests/hook-contracts.sh
- name: Subagent scope guard regression tests (#86)
run: bash hooks/subagent-scope-guard.test.sh
8 changes: 8 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Autonomous Dev Kit Release Notes

## v6.5.6 — 2026-06-22

Fix for issue #86: `subagent-scope-guard` false-positived on committed `.scope-lock` files and its block message instructed subagents to auto-revert them — producing spurious `git revert`/`reapply` commits from read-only subagents.

- `hooks/subagent-scope-guard`: removed the unattributed "did HEAD's last commit touch a `.scope-lock`" check. A committed `.scope-lock` is always legitimate (`hooks/scope-lock-apply` is the only sanctioned writer; the Write/Edit/MultiEdit tools are guarded against `*.scope-lock` paths), so the check could only false-positive — on every subagent stopping in a repo whose HEAD carried a committed lock (the steady state mid-pipeline). Manifest drift is still enforced by the uncommitted working-tree check (in-flight writes) and the push/PR + Stop hash guards (committed drift).
- `hooks/subagent-scope-guard`: the block message no longer instructs `git revert` (the instruction that made compliant subagents produce spurious commits). It now tells the subagent to run no git command, surface the finding to the lead, and notes that pre-existing committed locks need no action.
- `hooks/subagent-scope-guard.test.sh` (new): regression coverage — committed lock at HEAD does NOT block; uncommitted working-tree write DOES block with a non-destructive message; clean repo passes.

## v6.5.5 — 2026-06-22

Projects-layer alignment: the `project-design-guidance` skill now also consults the projects tracker (`docs/PROJECTS.md`) for current initiative focus + phase.
Expand Down
44 changes: 22 additions & 22 deletions hooks/subagent-scope-guard
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,17 @@ if command -v git >/dev/null 2>&1; then
done <<< "$scope_lock_dirty"
fi

# Last commit: did it touch scope-lock files?
if git rev-parse --verify HEAD >/dev/null 2>&1; then
last_commit_files=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null || true)

lock_in_commit=$(printf '%s' "$last_commit_files" | grep '\.scope-lock$' || true)
if [ -n "$lock_in_commit" ]; then
while IFS= read -r f; do
violations="${violations} • Last commit modified scope-lock file: ${f}\n"
done <<< "$lock_in_commit"
fi

fi
# NOTE: we intentionally do NOT inspect HEAD's last commit for .scope-lock
# files. A committed .scope-lock is always legitimate: hooks/scope-lock-apply
# is the only sanctioned writer (the Write/Edit/MultiEdit tools are guarded
# against *.scope-lock paths by pre-tool-scope-guard), so any committed lock
# was produced by the scope-lock skill or the operator. Checking HEAD's last
Comment on lines +66 to +70
# commit attributes that commit to whichever subagent stops next, false-
# positives on every subsequent subagent, and the old block message then
# instructed the subagent to `git revert` the legitimate lock — producing
# spurious revert/reapply commits (autonomous-dev-kit#86). Manifest drift is
# still enforced: by the uncommitted working-tree check above for in-flight
# writes, and by the push/PR + Stop hash guards for committed drift.

# Locked plan files may be edited for design backports or notes, but
# their Scope Manifest hash must still match the lock file.
Expand Down Expand Up @@ -126,17 +125,18 @@ fi

[ -z "$violations" ] && exit 0

block "Subagent stop blocked — protected files were modified:
block "Subagent stop blocked — protected file changes detected in the working tree:

${violations}
Subagents must not write .scope-lock files or change a Locked Scope Manifest
without the amendment path. Non-manifest design backports are allowed if the
manifest hash still verifies.

Before this subagent's changes are accepted:
1. Revert unauthorized .scope-lock or manifest changes.
2. If a manifest change is genuinely needed, surface it to the lead agent and
go through the amendment path: recording-decisions → update manifest →
re-run alignment-check.
The working-tree check cannot attribute these changes to a specific actor or
turn — they may predate this subagent (e.g. an uncommitted lock left by an
earlier step). If so, no action is needed.
Comment on lines +131 to +133
Do NOT run any git command (no revert/reset/checkout) and do NOT attempt to
'fix' this yourself — doing so has caused spurious revert/reapply commits in
the past (autonomous-dev-kit#86). Instead:
1. Surface this finding to the lead agent in your final report and stop.
2. The lead decides whether the change is legitimate or needs the amendment
path (recording-decisions → update manifest → re-run alignment-check →
re-run scope-lock-apply).

See skills/scope-lock/SKILL.md for the amendment path."
126 changes: 126 additions & 0 deletions hooks/subagent-scope-guard.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# hooks/subagent-scope-guard.test.sh
# Lifecycle test for the subagent-scope-guard SubagentStop hook.
#
# Regression coverage for autonomous-dev-kit#86:
# (1) a COMMITTED .scope-lock at HEAD (legitimate; written by scope-lock-apply)
# must NOT trigger a block — the old code inspected HEAD's last commit and
# false-positived on every subsequent subagent, then instructed `git revert`.
# (2) an UNCOMMITTED working-tree .scope-lock write (a subagent directly writing
# a lock) MUST trigger a block — but the message must be NON-destructive: it
# must NOT instruct the subagent to revert/run git (the #86 hazard), and must
# tell it to surface to the lead instead.
Comment on lines +5 to +12
# (3) a clean repo (no scope-lock changes) must NOT trigger a block.
#
# Run: bash hooks/subagent-scope-guard.test.sh

set -euo pipefail
Comment on lines +15 to +17

REPO_ROOT="$(cd "$(dirname -- "$0")/.." && pwd)"
HOOK="${REPO_ROOT}/hooks/subagent-scope-guard"

export LC_ALL=C LANG=C LC_CTYPE=C
export SUPERPOWERS_HOOKS_DISABLE= # ensure the guard is active during the test

failures=0
fail() { printf 'FAIL: %s\n' "$*" >&2; failures=$((failures + 1)); }
pass() { printf 'PASS: %s\n' "$*"; }

require_jq() {
if ! command -v jq >/dev/null 2>&1; then
printf 'SKIP: jq not found; subagent-scope-guard test requires jq\n'
exit 0
fi
}

# run_hook <cwd> — feeds a SubagentStop payload (stop_hook_active=false) on stdin.
run_hook() {
local cwd="$1"
jq -nc --arg cwd "$cwd" --arg tp "/tmp/fake-transcript-${RANDOM}.jsonl" \
'{stop_hook_active:false, cwd:$cwd, transcript_path:$tp, tool_name:"Agent"}' \
| env LC_ALL=C LANG=C LC_CTYPE=C "$HOOK"
}

setup_repo() {
# Create a temp git repo with one ordinary committed file (clean baseline).
local d
d="$(mktemp -d)"
git -C "$d" init -q
git -C "$d" config user.email t@t.t
git -C "$d" config user.name t
printf 'hello\n' > "$d/README.md"
git -C "$d" add README.md
git -C "$d" commit -q -m init
printf '%s' "$d"
}

require_jq

# ── Test 1: committed .scope-lock at HEAD must NOT block (the #86 bug) ──
{
repo="$(setup_repo)"
# simulate scope-lock-apply having written + committed the lock
mkdir -p "$repo/docs/plans"
printf 'manifest\n' > "$repo/docs/plans/x.plan.md"
printf 'abcdef0123456789\n' > "$repo/docs/plans/x.plan.md.scope-lock"
git -C "$repo" add docs/plans/x.plan.md docs/plans/x.plan.md.scope-lock
git -C "$repo" commit -q -m "lock scope"
out="$(run_hook "$repo" 2>/dev/null || true)"
if printf '%s' "$out" | jq -e '.decision=="block"' >/dev/null 2>&1; then
fail "Test1: committed lock at HEAD falsely blocked (the #86 bug): $(printf '%s' "$out" | jq -r .reason)"
else
pass "Test1: committed lock at HEAD does NOT block (regression fixed)"
fi
rm -rf "$repo"
}

# ── Test 2: uncommitted working-tree .scope-lock write MUST block, non-destructively ──
{
repo="$(setup_repo)"
# a subagent directly wrote a .scope-lock next to an already-tracked plan
# (the realistic case: the plan dir is tracked, so the new lock shows as a
# specific untracked file rather than a collapsed untracked dir).
mkdir -p "$repo/docs/plans"
printf '## Scope Manifest\n\n(locked content)\n' > "$repo/docs/plans/y.plan.md"
git -C "$repo" add docs/plans/y.plan.md
git -C "$repo" commit -q -m "add plan"
printf 'tampered\n' > "$repo/docs/plans/y.plan.md.scope-lock"
out="$(run_hook "$repo" 2>/dev/null || true)"
if ! printf '%s' "$out" | jq -e '.decision=="block"' >/dev/null 2>&1; then
fail "Test2: uncommitted .scope-lock write did NOT block (should be caught)"
else
pass "Test2: uncommitted .scope-lock write is blocked (legitimate catch)"
reason="$(printf '%s' "$out" | jq -r .reason)"
# Non-destructive: the OLD "Revert unauthorized" instruction must be GONE
if printf '%s' "$reason" | grep -qi 'Revert unauthorized'; then
fail "Test2: block message still instructs 'Revert unauthorized' (the #86 hazard)"
else
pass "Test2: block message is non-destructive (no 'Revert unauthorized' instruction)"
fi
# And it must tell the subagent to surface to the lead / run no git
if printf '%s' "$reason" | grep -qi 'Surface this finding to the lead'; then
pass "Test2: block message directs subagent to surface to the lead"
else
fail "Test2: block message does not direct subagent to surface to the lead"
fi
fi
rm -rf "$repo"
}

# ── Test 3: clean repo must NOT block ──
{
repo="$(setup_repo)"
out="$(run_hook "$repo" 2>/dev/null || true)"
if printf '%s' "$out" | jq -e '.decision=="block"' >/dev/null 2>&1; then
fail "Test3: clean repo falsely blocked"
else
pass "Test3: clean repo does NOT block"
fi
rm -rf "$repo"
}

if [ "$failures" -gt 0 ]; then
printf '\n%d failure(s)\n' "$failures" >&2
exit 1
fi
printf '\nAll subagent-scope-guard tests passed.\n'
Loading