Files
holocron/scripts/check-scope-walkup-sync.sh
Defame1297 620f20b0fd refactor(kyberforge)!: merge skill-audit and agent-audit into factory-audit
Why

The two audit skills carried 1,724 lines of byte-identical duplication: the ADR-0020 boundary
resolver (1,061), vale-wrap.sh (526), the Vale style rules (44) and the Contributing-files parser
(93). Nothing shared them — they were held in sync by a 413-line pre-push gate and its 797-line
test suite. Sync-by-gate had already failed once: at 484357a the two parser copies drifted into
different spellings of the bullet loop while a docstring asserted they were identical. That drift
was behaviour-neutral and was re-unified by hand at 598a7c3, so the copies were identical at merge
time — but nothing had caught it, and the next drift need not be neutral.

Implementation Notes

Self-containment binds BETWEEN skills, not within one. The agentskills.io spec forbids reaching
across skill directories, which is why two separate skills needed embedded copies; two files inside
ONE skill may source a third. That is the whole reason the merge removes duplication rather than
relocating it.

The union of both bodies measured 1,532 words against BODY_MAX_WORDS=900, and only 211 of those
words were shared, so SKILL.md is a dispatch body. Step 0 resolves the flow from the target path
before any validation, and its table mirrors validate.sh's detection exactly: a directory holding
SKILL.md or a SKILL.md file (skill); a *.agent.md, or a .md directly under an agents/ directory
(agent); anything else stops without running a validator. Steps 1-3 live in
references/skill-flow.md and references/agent-flow.md, and gotchas that apply to one flow live in
that flow's file, since it is loaded on every invocation anyway. If validate.sh reports on the
other artifact type, the body restarts at Step 0.

Named factory-audit rather than forge-audit because forge is a live skill, and a family prefix that
matches a live sibling reads as ownership rather than membership.

The description carries one arrow per boundary target, because ADR-0020 resolves only the first
target after an arrow. It drops the quoted "audit this skill"-style phrases, which restated
"audited" in a second register (ADR-0020's duplicate-register rule). 241 characters, Gotchas 16%
of the body: no size SUGGESTIONs.

The boundary resolver stays embedded in two files rather than imported: a cache-installed plugin
cannot read outside its own directory, and the repo-root hook resolves via .pre-commit-hooks.yaml
where entry[0] is the only token pre-commit rewrites, so no single file is reachable by both.
tests/test-adr0020-contract.sh hashes both copies for byte-identity, and asserts validate.sh sources
the resolver and that no third copy exists.

The entry scripts classify the target from its resolved parent directory, so a bare agent filename
typed inside agents/ works; resolve SCRIPT_DIR CDPATH-safely; and exit 2 when a lib-*.sh is
missing, rather than dying with exit 1, the tier the flows relay as real findings.

The provenance run functions stash their findings code in KYBERFORGE_PROV_RC and
return 0, so validate-provenance.sh calls them UNTESTED. Testing a function's
status (`f || RC=$?`) disables errexit for its entire body, and no subshell or
`set -e` inside can re-arm it once the call sits in a condition context
(measured, both spellings). Their error paths use `exit`, which is unaffected
either way; this keeps errexit armed for anything added later.

Case 0's readability guard reads the file instead of asking `[[ -r ]]`. `-r` is
access(2), which answers yes for uid 0 even on a mode-000 file, and this repo's
dev environment is root -- so the guard could never fire where it exists to fire.
A read attempt is also the stricter question, catching EIO. This is the reasoning
scripts/check-vale-style-sync.sh carried before this commit deleted it; the
hazard did not go with it.

All three entry scripts are CDPATH-safe, vale-wrap.sh included: both of its cd sites are cleared,
the --config resolution and the directory-mirror walk, where an exported CDPATH would otherwise
print a decoy path into the -print0 stream and build the mirror from the decoy's files. The two
remaining bare cd calls take absolute paths, which CDPATH is never consulted for.

Impact

BREAKING: skill-audit and agent-audit no longer exist as invocable skills. kyberforge goes to
2.0.0 (catalog 0.4.7).

Check logic is unchanged: differential runs of the old and new validators across every skill and
agent produced byte-identical stdout, stderr and exit codes, and the reconstructed Python payloads
differ only in comments and the references/field-inventory.md -> agent-field-inventory.md rename.
One doctrine governs the tiers: exit 0 is audited and clean, exit 1 is audited with findings OR a
target present but unreadable, exit 2 is that nothing was audited at all. Edge paths DID change,
deliberately (full table in ADR-0025):
- a missing target exits 2 (never ran), not 1, under its own "does not exist" message; detection is
  by path shape, so a shape-matching path that is simply absent used to reach the validator and come
  back as a FAIL against a file that never existed;
- an unshaped target exits 2 under the generic "matches neither" message, and a directory with no
  SKILL.md under a third, distinct one -- three exit-2 messages, not one;
- a dangling symlink or a symlink loop stays exit 1: it is present but broken, which is a finding
  about the artifact rather than a usage error;
- a SKILL.md file path is audited as its skill directory instead of refused;
- a .md agent outside an agents/ directory is refused rather than audited;
- a missing script library, a missing python3, a missing PyYAML, and no argument at all each exit 2.
  validate-provenance.sh already exited 2 for the last two; validate.sh now matches it.

.pre-commit-hooks.yaml is a published contract consumed by external repos. Both hook IDs and both
files: regexes are unchanged; only entry: and description: moved.

scripts/check-vale-style-sync.sh (413), scripts/sync-vale-styles.sh (21),
tests/test-check-vale-style-sync.sh (797) and agent-audit/scripts/README.md (47) are deleted. The
checker made 17 assertions: 6 compared the two Vale copies and are moot; 10 are rehomed into
tests/test-vale-wrap.sh (case 0, cases 28-31, and the suite's Vale-absent skip); and the
cross-manifest files: agreement check, which selected hooks by entry: and so could not survive both
hooks sharing one, is ported as case 33 pairing hooks by id:. Cases 28, 30 and 33 carry mutation
self-tests; narrowing the local skill prefilter to 6 of 38 SKILL.md files now fails the suite.

Skills go 39 to 38. Pre-push goes 9 repo-authored hooks to 8.

ADR: 0025
BREAKING-CHANGE: the skill-audit and agent-audit skills are removed. Both flows are served by
  factory-audit, which auto-detects whether it was handed a skill directory or an agent file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
2026-09-16 09:13:57 +00:00

382 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Behavioral consistency check for the four independent, semantically-equivalent
# ports of "walk up from a directory looking for a scope-defining marker" living
# in this repo:
#
# - plugins/kyberforge/.apm/skills/factory-audit/scripts/validate.sh (Python: detect_scope)
# - plugins/kyberforge/.apm/skills/factory-audit/scripts/validate-provenance.sh (Python: find_plugin_root)
# - plugins/kyberforge/.apm/skills/agent-author/scripts/new-agent.sh (Bash: find_package_root)
# - plugins/kyberforge/.apm/skills/skill-author/scripts/new-skill.sh (Bash: find_package_root)
#
# THIS GATE SURVIVED ADR-0025 AND ITS JOB DID NOT SHRINK. The merge of
# skill-audit and agent-audit into factory-audit moved the first two ports into
# one directory; it did not merge the ports. Two of the four still live in
# agent-author and skill-author, which ADR-0020 deliberately left unmerged, so
# the walk-up still has four independent implementations across three skill
# directories and this is still the only thing comparing them.
#
# It also still cannot become a text diff, and the reason is not the one that
# retired check-vale-style-sync.sh alongside the merge. That gate diffed two
# literal copies of the same file, so collapsing them to one copy left it
# nothing to compare. These four are not copies of anything: they are hand-
# ported reimplementations of the same walk-up in two different languages —
# Python in factory-audit's two validators, Bash in the two authors' scaffold
# scripts. There is no byte sequence common to a Python function and a Bash
# function that agreeing on behavior would preserve. Sourcing cannot close the
# gap either: per ADR-0014's no-cross-skill-path rule a cache-installed skill
# copies only its own directory, which is exactly why factory-audit's two
# validators CAN now source a shared library from their own scripts/ while the
# author skills across the boundary still cannot.
#
# So this asserts behavioral agreement instead: it builds a matrix of fixture
# directory trees and checks that whatever new-agent.sh/new-skill.sh actually
# create on disk, validate.sh/validate-provenance.sh classify the same way when
# pointed at the result. Run from repo root or pass REPO_ROOT as arg.
REPO_ROOT="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
if [[ ! -d "$REPO_ROOT" ]]; then
echo "Scope walk-up sync check failed: REPO_ROOT '$REPO_ROOT' is not a directory." >&2
exit 1
fi
REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
NEW_AGENT="$REPO_ROOT/plugins/kyberforge/.apm/skills/agent-author/scripts/new-agent.sh"
NEW_SKILL="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-author/scripts/new-skill.sh"
VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/factory-audit/scripts/validate.sh"
VALIDATE_PROVENANCE="$REPO_ROOT/plugins/kyberforge/.apm/skills/factory-audit/scripts/validate-provenance.sh"
# Floor on the four hardcoded `plugins/kyberforge/.apm/...` paths above. A
# missing target is only a legitimate no-op for a repo that has no kyberforge
# plugin at all; if `plugins/kyberforge/` IS here and the `.apm/` script under it
# is not, these paths have gone stale and every fixture below silently does not
# run. The whole exit is 0 either way, so a stale path is indistinguishable from
# "all four implementations agree" — and a path rewrite is exactly the kind of
# edit that would slip through it. Same reasoning as the REPO_ROOT guard above.
for f in "$NEW_AGENT" "$NEW_SKILL" "$VALIDATE" "$VALIDATE_PROVENANCE"; do
if [[ ! -f "$f" ]]; then
if [[ -d "$REPO_ROOT/plugins/kyberforge" ]]; then
echo "Scope walk-up sync check failed: $REPO_ROOT/plugins/kyberforge exists but $f does not — this script's .apm/ paths have gone stale, so none of the walk-up fixtures ran. Update them to wherever the factory-audit/agent-author/skill-author scripts now live." >&2
exit 1
fi
echo "Scope walk-up sync check: $f not found — kyberforge factory-audit/agent-author/skill-author skills not present, nothing to check." >&2
exit 0
fi
done
FAIL=0
err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
ok() { echo " ok: $1"; }
FIXTURES=()
cleanup() { [[ ${#FIXTURES[@]} -eq 0 ]] || rm -rf "${FIXTURES[@]}"; }
trap cleanup EXIT
# Per-run scratch directory for the per-fixture output captures below. These
# used to be fixed paths in the shared system temp directory, which made this
# script non-reentrant: tests/run-tests.sh now fans its scripts out
# concurrently, and two instances sharing one path clobber each other's
# captures. Fixture 6 reads its capture back (`[[ -z "$(cat ...)" ]]`), so a
# cross-run write there silently flips a real verdict, and a pre-existing
# directory sitting at one of the paths breaks the run outright. Keep these
# under a per-run mktemp -d; tests/test-check-scope-walkup-sync.sh asserts it.
# Registered in FIXTURES so the single cleanup trap already here removes it.
RUN_TMP="$(mktemp -d)"
FIXTURES+=("$RUN_TMP")
# Fill a new-agent.sh-scaffolded pair's FILL IN: placeholders with valid
# content, isolating the scope/counterpart-lookup question from unrelated
# content-quality FAILs when cross-checking against validate.sh.
fill_agent_pair() {
local file="$1" name="$2"
cat > "$file" <<EOF
---
name: ${name}
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
}
# ---------------------------------------------------------------------------
# Fixture 1: root exactly $HOME (no walk-up) — new-agent.sh's own documented
# user-scope case.
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: root exactly \$HOME ---"
F1_HOME="$(mktemp -d)"
FIXTURES+=("$F1_HOME")
NAME1="probe-home-exact"
if ! env HOME="$F1_HOME" bash "$NEW_AGENT" "$NAME1" "$F1_HOME" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold at root exactly \$HOME"
else
if [[ ! -f "$F1_HOME/.claude/agents/$NAME1.md" || ! -f "$F1_HOME/.copilot/agents/$NAME1.agent.md" ]]; then
err "new-agent.sh did not create the expected user-scope pair at \$HOME/.claude and \$HOME/.copilot"
else
fill_agent_pair "$F1_HOME/.claude/agents/$NAME1.md" "$NAME1"
fill_agent_pair "$F1_HOME/.copilot/agents/$NAME1.agent.md" "$NAME1"
if env HOME="$F1_HOME" bash "$VALIDATE" "$F1_HOME/.claude/agents/$NAME1.md" >"$RUN_TMP/f1.out" 2>&1; then
ok "validate.sh agrees: user scope, counterpart found under \$HOME/.copilot"
else
err "validate.sh disagreed with new-agent.sh's user-scope classification at root exactly \$HOME"
sed 's/^/ /' "$RUN_TMP/f1.out"
fi
fi
fi
# ---------------------------------------------------------------------------
# Fixture 2: nested marker-less directory under $HOME — the live-repro shape.
# new-agent.sh's own docs call this out as deliberately project scope, not
# user scope (a stray directory under $HOME can't be silently redirected into
# the shared global ~/.claude or ~/.copilot agent directories).
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: nested marker-less directory under \$HOME ---"
F2_HOME="$(mktemp -d)"
FIXTURES+=("$F2_HOME")
F2_NESTED="$F2_HOME/scratch/testdir"
mkdir -p "$F2_NESTED"
NAME2="probe-home-nested"
if ! env HOME="$F2_HOME" bash "$NEW_AGENT" "$NAME2" "$F2_NESTED" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold under a nested marker-less \$HOME subdirectory"
else
if [[ ! -f "$F2_NESTED/.claude/agents/$NAME2.md" || ! -f "$F2_NESTED/.github/agents/$NAME2.agent.md" ]]; then
err "new-agent.sh did not scaffold a project-scope pair at the nested dir (rooted at \$F2_NESTED, not \$HOME)"
elif [[ -f "$F2_HOME/.claude/agents/$NAME2.md" || -f "$F2_HOME/.copilot/agents/$NAME2.agent.md" ]]; then
err "new-agent.sh unexpectedly wrote into \$HOME/.claude or \$HOME/.copilot for a nested marker-less start dir"
else
ok "new-agent.sh: nested marker-less dir under \$HOME scaffolds project scope at the nested dir"
fill_agent_pair "$F2_NESTED/.claude/agents/$NAME2.md" "$NAME2"
fill_agent_pair "$F2_NESTED/.github/agents/$NAME2.agent.md" "$NAME2"
if env HOME="$F2_HOME" bash "$VALIDATE" "$F2_NESTED/.claude/agents/$NAME2.md" >"$RUN_TMP/f2.out" 2>&1; then
ok "validate.sh agrees: project scope, counterpart found at the nested dir (not \$HOME/.copilot)"
else
err "validate.sh disagreed with new-agent.sh: misclassified the nested marker-less \$HOME subdirectory"
sed 's/^/ /' "$RUN_TMP/f2.out"
fi
# new-skill.sh has no user/project distinction of its own (no $HOME
# awareness at all — see new-skill.sh's find_package_root), but it shares
# the same .git/apm.yml walk-up primitive. It must land its standalone
# scaffold at the given path too, not get redirected toward $HOME.
if env HOME="$F2_HOME" bash "$NEW_SKILL" probe-home-nested-skill "$F2_NESTED" >"$RUN_TMP/f2skill.out" 2>&1 \
&& [[ -d "$F2_NESTED/probe-home-nested-skill" ]]; then
ok "new-skill.sh agrees: standalone mode scaffolds at the nested dir, not redirected toward \$HOME"
else
err "new-skill.sh disagreed with new-agent.sh/validate.sh on the nested marker-less \$HOME subdirectory"
sed 's/^/ /' "$RUN_TMP/f2skill.out"
fi
fi
fi
# ---------------------------------------------------------------------------
# Fixture 3: a .git boundary between the probe dir and $HOME must stop the
# walk before it ever reaches $HOME (so it can't be misclassified as user
# scope via the home-boundary path).
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: .git boundary short-circuits before reaching \$HOME ---"
F3_HOME="$(mktemp -d)"
FIXTURES+=("$F3_HOME")
# .git sits directly at the probe root (the conventional two-segments-above
# location .claude/agents and .github/agents are placed relative to). This
# fixture only exercises what it's meant to: that a .git ancestor stops the
# walk before it ever reaches $HOME. Fixture 3b below covers .git sitting
# higher up than the probe root.
F3_PROBE="$F3_HOME/myrepo"
mkdir -p "$F3_PROBE/.git"
NAME3="probe-git-boundary"
if ! env HOME="$F3_HOME" bash "$NEW_AGENT" "$NAME3" "$F3_PROBE" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold at a dir with a .git ancestor short of \$HOME"
else
if [[ ! -f "$F3_PROBE/.claude/agents/$NAME3.md" || ! -f "$F3_PROBE/.github/agents/$NAME3.agent.md" ]]; then
err "new-agent.sh did not scaffold a project-scope pair at the probe dir"
else
fill_agent_pair "$F3_PROBE/.claude/agents/$NAME3.md" "$NAME3"
fill_agent_pair "$F3_PROBE/.github/agents/$NAME3.agent.md" "$NAME3"
if env HOME="$F3_HOME" bash "$VALIDATE" "$F3_PROBE/.claude/agents/$NAME3.md" >"$RUN_TMP/f3.out" 2>&1; then
ok "validate.sh agrees: .git boundary keeps this project scope, not promoted to user scope at \$HOME"
else
err "validate.sh disagreed with new-agent.sh on the .git-boundary-before-\$HOME fixture"
sed 's/^/ /' "$RUN_TMP/f3.out"
fi
fi
fi
# ---------------------------------------------------------------------------
# Fixture 3b: .git sits one level ABOVE the probe root — a subdirectory of a
# larger git-tracked tree (e.g. a monorepo package dir). new-agent.sh always
# places project-scope files at its ROOT argument, never at the walked-up
# .git location, so validate.sh must resolve scope to the probe root too, not
# to the ancestor where .git happened to be found.
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: .git ancestor sits above <root> (subdirectory of a larger git tree) ---"
F3B_REPO="$(mktemp -d)"
FIXTURES+=("$F3B_REPO")
mkdir -p "$F3B_REPO/.git"
F3B_PROBE="$F3B_REPO/subdir"
mkdir -p "$F3B_PROBE"
NAME3B="probe-git-above-root"
if ! bash "$NEW_AGENT" "$NAME3B" "$F3B_PROBE" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold at a dir one level below a .git ancestor"
else
if [[ ! -f "$F3B_PROBE/.claude/agents/$NAME3B.md" || ! -f "$F3B_PROBE/.github/agents/$NAME3B.agent.md" ]]; then
err "new-agent.sh did not scaffold a project-scope pair at the probe dir (rooted at \$F3B_PROBE, not the repo root)"
else
fill_agent_pair "$F3B_PROBE/.claude/agents/$NAME3B.md" "$NAME3B"
fill_agent_pair "$F3B_PROBE/.github/agents/$NAME3B.agent.md" "$NAME3B"
if bash "$VALIDATE" "$F3B_PROBE/.claude/agents/$NAME3B.md" >"$RUN_TMP/f3b.out" 2>&1; then
ok "validate.sh agrees: scope root is <root>, not the .git ancestor above it"
else
err "validate.sh disagreed with new-agent.sh: resolved scope to the .git ancestor instead of <root>"
sed 's/^/ /' "$RUN_TMP/f3b.out"
fi
fi
fi
# ---------------------------------------------------------------------------
# Fixture 4: a type-bearing apm.yml — plugin/APM scope. new-agent.sh and
# new-skill.sh must agree on the same package root, and validate.sh /
# validate-provenance.sh must both recognize it as plugin scope.
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: type-bearing apm.yml (plugin/APM scope) ---"
F4_ROOT="$(mktemp -d)"
FIXTURES+=("$F4_ROOT")
printf 'name: test-package\nversion: 0.1.0\ntype: skill\n' > "$F4_ROOT/apm.yml"
NAME4="probe-plugin"
if ! bash "$NEW_AGENT" "$NAME4" "$F4_ROOT" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold at a type-bearing apm.yml root"
elif [[ ! -f "$F4_ROOT/.apm/agents/$NAME4.agent.md" ]]; then
err "new-agent.sh did not scaffold plugin scope at the type-bearing apm.yml root"
else
ok "new-agent.sh: plugin scope at type-bearing apm.yml root"
if bash "$NEW_SKILL" probe-plugin-skill "$F4_ROOT" >"$RUN_TMP/f4skill.out" 2>&1 \
&& [[ -d "$F4_ROOT/.apm/skills/probe-plugin-skill" ]]; then
ok "new-skill.sh agrees: package mode at the same apm.yml root"
else
err "new-skill.sh disagreed with new-agent.sh on the type-bearing apm.yml root"
sed 's/^/ /' "$RUN_TMP/f4skill.out"
fi
fill_agent_pair "$F4_ROOT/.apm/agents/$NAME4.agent.md" "$NAME4"
if bash "$VALIDATE" "$F4_ROOT/.apm/agents/$NAME4.agent.md" >"$RUN_TMP/f4validate.out" 2>&1; then
ok "validate.sh agrees: plugin/APM scope, structural checks pass"
else
err "validate.sh disagreed with new-agent.sh: did not treat the type-bearing apm.yml root as plugin scope"
sed 's/^/ /' "$RUN_TMP/f4validate.out"
fi
# source_keys + a matching sources.md round-trips only if validate-provenance.sh
# resolves the SAME plugin root new-agent.sh/new-skill.sh did.
cat > "$F4_ROOT/.apm/agents/$NAME4.agent.md" <<EOF
---
name: ${NAME4}
description: A valid agent description.
source_keys:
- probe-source
---
You are a test agent.
EOF
cat > "$F4_ROOT/sources.md" <<EOF
# Sources
## probe-source
- **URL:** https://example.com/probe-source
- **Description:** A test source.
- **Contributing files:** .apm/agents/${NAME4}.agent.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
if bash "$VALIDATE_PROVENANCE" "$F4_ROOT/.apm/agents/$NAME4.agent.md" >"$RUN_TMP/f4prov.out" 2>&1; then
ok "validate-provenance.sh agrees: resolves the same plugin root, sources.md round-trips"
else
err "validate-provenance.sh disagreed on the plugin root for the type-bearing apm.yml fixture"
sed 's/^/ /' "$RUN_TMP/f4prov.out"
fi
fi
# ---------------------------------------------------------------------------
# Fixture 5: filesystem-boundary fallback — no $HOME relation, no marker
# anywhere. Both scripts must fall through to project scope, not user scope.
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: filesystem-boundary fallback (no \$HOME relation, no markers) ---"
F5_UNRELATED_HOME_PARENT="$(mktemp -d)"
FIXTURES+=("$F5_UNRELATED_HOME_PARENT")
F5_UNRELATED_HOME="$F5_UNRELATED_HOME_PARENT/never-reached-$$"
F5_ROOT="$(mktemp -d)/deep/proj"
mkdir -p "$F5_ROOT"
FIXTURES+=("$(dirname "$(dirname "$F5_ROOT")")")
NAME5="probe-fs-boundary"
if ! env HOME="$F5_UNRELATED_HOME" bash "$NEW_AGENT" "$NAME5" "$F5_ROOT" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold at the filesystem-boundary fixture"
else
if [[ ! -f "$F5_ROOT/.claude/agents/$NAME5.md" || ! -f "$F5_ROOT/.github/agents/$NAME5.agent.md" ]]; then
err "new-agent.sh did not scaffold project scope at the filesystem-boundary fixture"
else
fill_agent_pair "$F5_ROOT/.claude/agents/$NAME5.md" "$NAME5"
fill_agent_pair "$F5_ROOT/.github/agents/$NAME5.agent.md" "$NAME5"
if env HOME="$F5_UNRELATED_HOME" bash "$VALIDATE" "$F5_ROOT/.claude/agents/$NAME5.md" >"$RUN_TMP/f5.out" 2>&1; then
ok "validate.sh agrees: filesystem-boundary fallback resolves to project scope"
else
err "validate.sh disagreed with new-agent.sh on the filesystem-boundary fallback fixture"
sed 's/^/ /' "$RUN_TMP/f5.out"
fi
fi
fi
# ---------------------------------------------------------------------------
# Fixture 6: a type-bearing apm.yml ABOVE $HOME must not be reached by
# validate-provenance.sh's walk-up from a nested, marker-less dir under $HOME
# — matches new-agent.sh, which also stops at $HOME before ever looking that
# far up.
# ---------------------------------------------------------------------------
echo ""
echo "--- fixture: type-bearing apm.yml above \$HOME must not be reached ---"
F6_ANCESTOR="$(mktemp -d)"
FIXTURES+=("$F6_ANCESTOR")
printf 'name: outer-package\nversion: 0.1.0\ntype: skill\n' > "$F6_ANCESTOR/apm.yml"
F6_HOME="$F6_ANCESTOR/fakehome"
mkdir -p "$F6_HOME"
NAME6="probe-above-home"
if ! env HOME="$F6_HOME" bash "$NEW_AGENT" "$NAME6" "$F6_HOME" >/dev/null 2>&1; then
err "new-agent.sh failed to scaffold with a type-bearing apm.yml above \$HOME"
elif [[ -f "$F6_HOME/.apm/agents/$NAME6.agent.md" ]]; then
err "new-agent.sh walked past \$HOME and misclassified as plugin scope using the ancestor apm.yml"
elif [[ ! -f "$F6_HOME/.claude/agents/$NAME6.md" ]]; then
err "new-agent.sh did not scaffold user scope at root exactly \$HOME (with a type-bearing apm.yml above)"
else
ok "new-agent.sh: \$HOME boundary stops the walk before the ancestor apm.yml, user scope at \$HOME"
mkdir -p "$F6_HOME/.apm/agents"
cat > "$F6_HOME/.apm/agents/probe-prov.agent.md" <<'EOF'
---
name: probe-prov
description: A valid agent description.
source_keys:
- probe-source
---
You are a test agent.
EOF
# No sources.md exists anywhere under $F6_HOME or at the ancestor package
# root — if find_plugin_root walked past $HOME to the ancestor apm.yml,
# this would FAIL on Check 0 (source_keys declared but sources.md absent).
if env HOME="$F6_HOME" bash "$VALIDATE_PROVENANCE" "$F6_HOME/.apm/agents/probe-prov.agent.md" >"$RUN_TMP/f6.out" 2>&1 \
&& [[ -z "$(cat "$RUN_TMP/f6.out")" ]]; then
ok "validate-provenance.sh agrees: \$HOME boundary stops the walk, exits 0 silently (not plugin scope)"
else
err "validate-provenance.sh walked past \$HOME to the ancestor apm.yml — disagrees with new-agent.sh"
sed 's/^/ /' "$RUN_TMP/f6.out"
fi
fi
echo ""
if [[ $FAIL -gt 0 ]]; then
echo "Scope walk-up sync check failed: $FAIL error(s). One of validate.sh's detect_scope, validate-provenance.sh's find_plugin_root, new-agent.sh's find_package_root, or new-skill.sh's find_package_root has drifted from the others' \$HOME/.git/apm.yml walk-up semantics. Re-read new-agent.sh's usage comment (the canonical description of the intended behavior) and bring the disagreeing script back in line." >&2
exit 1
fi
echo "Scope walk-up sync check passed: all four walk-up implementations agree on every fixture."