fix(gates): make the ADR-0020 boundary check parse what skills actually write
The routing-target check understood only a single-arrow clause naming a bare skill, so most real boundary prose was silently skipped rather than verified. Two of those silences were fail-open: an unrecognised token following a target dropped that target from the check entirely, and a skill directory with no SKILL.md still resolved as a valid routing target, so a broken route passed. Multi-target arrow clauses now draw a SUGGESTION instead of being ignored, hand-invocation phrasing is carved out so it is not read as a route, and a dotted filename parses into a new `unparsed` status rather than disappearing. Three test fixtures had been relying on the SKILL.md-less directory resolving as a target; they are corrected alongside the check. Addresses #107, #108, #110.
This commit is contained in:
@@ -70,13 +70,23 @@ PY
|
||||
# this repo's live skills. Echoes the subject skill's directory.
|
||||
#
|
||||
# <root>/plugins/fixture-plugin/.apm/skills/<subject>/SKILL.md
|
||||
# <root>/plugins/fixture-plugin/.apm/skills/fixture-sibling-skill/
|
||||
# <root>/plugins/fixture-plugin/.apm/skills/fixture-sibling-skill/SKILL.md
|
||||
# <root>/plugins/fixture-plugin/.apm/agents/fixture-sibling-agent.agent.md
|
||||
#
|
||||
# The sibling gets a real SKILL.md, and that is load-bearing rather than
|
||||
# tidiness: a directory under skills/ is a resolvable name only when it
|
||||
# HOLDS one. An empty leftover directory is untracked by git, so counting
|
||||
# one made a target resolve on the machine that made it and dangle in a
|
||||
# fresh clone. This helper used to mkdir the sibling and write nothing into
|
||||
# it, so the corroborator every blocking-tier test depends on silently
|
||||
# stopped resolving the moment that rule was enforced.
|
||||
make_fixture_tree() {
|
||||
local root="$1" subject="$2"
|
||||
local apm="$root/plugins/fixture-plugin/.apm"
|
||||
mkdir -p "$apm/skills/$subject" "$apm/skills/fixture-sibling-skill" "$apm/agents"
|
||||
touch "$apm/agents/fixture-sibling-agent.agent.md"
|
||||
make_sized_skill "$apm/skills/fixture-sibling-skill" \
|
||||
"Use when doing the other thing. Do not use for anything else." 10
|
||||
echo "$apm/skills/$subject"
|
||||
}
|
||||
}
|
||||
@@ -568,3 +578,168 @@ EOF
|
||||
assert_output --partial "boundary-target resolution DID NOT RUN"
|
||||
assert_output --partial "Unchecked target(s): some-other-skill"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 — the hand-invocation carve-out (issue #108)
|
||||
#
|
||||
# A skill carrying `disable-model-invocation: true` is absent from the
|
||||
# model-visible listing entirely: not preloaded, and the Skill tool refuses to
|
||||
# call it. Its description is never matched against user intent, so
|
||||
# references/description-quality.md Step 0 gives it ONE plain human-facing
|
||||
# sentence — no trigger list, no boundary clause — and calls a
|
||||
# missing-boundary-clause finding on such a skill "a wrong finding, not a strict
|
||||
# one". Until this ran, nothing here knew the field existed, so the audit
|
||||
# reported exactly the shape its own rubric mandates, with advice naming a
|
||||
# router that cannot see the skill.
|
||||
#
|
||||
# The carve-out is narrow. Both size gates are unaffected and both are pinned
|
||||
# below: the body is loaded on invocation like any other body, and the
|
||||
# 400-character ceiling is an outlier stop rather than a routing budget.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Helper: a skill directory carrying `disable-model-invocation: true`.
|
||||
make_hand_invoked_skill() {
|
||||
local dir="$1" desc="$2" body_words="$3"
|
||||
local name
|
||||
name="$(basename "$dir")"
|
||||
mkdir -p "$dir"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: $desc"
|
||||
echo "disable-model-invocation: true"
|
||||
echo "---"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * $body_words))"
|
||||
} > "$dir/SKILL.md"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a hand-invoked skill is not asked for a boundary clause" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_hand_invoked_skill "$skill" \
|
||||
"Tell the agent to zoom out and give broader context or a higher level perspective." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "has no boundary clause"
|
||||
assert_output --partial "hand-invoked"
|
||||
}
|
||||
|
||||
@test "ADR-0020: the SAME description without the flag IS asked for a boundary clause" {
|
||||
# The control. Without it the case above is satisfied by an audit that
|
||||
# stopped checking boundary clauses altogether.
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" \
|
||||
"Tell the agent to zoom out and give broader context or a higher level perspective." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "has no boundary clause"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a hand-invoked skill is exempt from the 250-character description target" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_hand_invoked_skill "$skill" \
|
||||
"$(python3 -c "print('Tell the agent to zoom out. ' + 'x' * 273)")" 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "over the 250-character"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a hand-invoked description over 400 chars still FAILS" {
|
||||
# The half the carve-out does NOT lift. 400 is an outlier stop, not a
|
||||
# routing-quality target: a hand-invoked description is still the one line
|
||||
# the user reads when choosing from the `/` menu.
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_hand_invoked_skill "$skill" \
|
||||
"$(python3 -c "print('Tell the agent to zoom out. ' + 'x' * 374)")" 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "400-character"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a hand-invoked body over 900 words still FAILS" {
|
||||
# The body is loaded on invocation exactly like any other body and competes
|
||||
# with the caller's live conversation the same way, so no body tier moves.
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_hand_invoked_skill "$skill" "Tell the agent to zoom out." 901
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "900-word"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 — one arrow, one target (issue #107)
|
||||
#
|
||||
# Only the FIRST target after an arrow was resolved: the conjunction
|
||||
# continuation is wired to the prose route verbs and never to arrows. So this
|
||||
# script printed "1 of 1 boundary target(s) resolve" on a clause naming two,
|
||||
# and the second was resolved by nothing and reported by nothing. A typo in it
|
||||
# shipped through a green gate. The shape is now rejected rather than the
|
||||
# extractor widened.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "ADR-0020: an arrow clause naming two targets is reported, not silently half-checked" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
# A bare `Not ... ->` sentence carries no BOUNDARY_MARKER, so the backtick
|
||||
# sweep does not run and the second target is invisible to every other rule
|
||||
# in the resolver — this is the exact shape #107 measured.
|
||||
make_sized_skill "$skill" "Use when doing the thing. Not the other thing -> \`fixture-sibling-skill\` or \`fixture-missing-second\`." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "names more than one target"
|
||||
}
|
||||
|
||||
@test "ADR-0020: one arrow per target — the convention the suggestion asks for — is silent" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
make_sized_skill "$skill" "Use when doing the thing. Not the other thing -> \`fixture-sibling-skill\`." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "names more than one target"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 — a dotted filename in a boundary clause (issue #110)
|
||||
#
|
||||
# `[^.;]` could not cross the `.` in `AGENTS.md`, so a clause naming a dotted
|
||||
# file between "Not" and the arrow was invisible. With a backticked target that
|
||||
# was a MISDIAGNOSIS — "no boundary clause" reported on a clause that was
|
||||
# present and working. With a BARE target it was worse: the target was never
|
||||
# extracted, so the dangling check silently did not run on it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "ADR-0020: a boundary clause naming a dotted filename is not reported as missing" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
make_sized_skill "$skill" "Use when doing the thing. Not AGENTS.md -> \`fixture-sibling-skill\`." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "has no boundary clause"
|
||||
assert_output --partial "description has a boundary clause"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a BARE target after a dotted filename is extracted and checked" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
# The silent half of #110: this clause produced no target at all, so it was
|
||||
# neither resolved nor reported — a route to a non-existent skill shipping
|
||||
# through a green gate with no finding of any kind.
|
||||
make_sized_skill "$skill" "Use when doing the thing. Not AGENTS.md -> fixture-missing-dotted." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "routes to 'fixture-missing-dotted'"
|
||||
}
|
||||
|
||||
@test "ADR-0020: an arrow clause yielding no target is reported as unparsed, not as missing" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
# A single-word target is deliberately not matchable bare, because
|
||||
# `research`, `triage` and `forge` are all skill names AND ordinary English.
|
||||
# The clause is present; saying it is missing sends the author to add a
|
||||
# second copy of a clause that is already there.
|
||||
make_sized_skill "$skill" "Use when doing the thing. Not the other thing -> forge." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "has no boundary clause"
|
||||
assert_output --partial "no target could be read"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user