fix(kyberforge): fix scope walk-up and manifest-parsing bugs from PR #93 review
A fresh /code-review of the APM-native authoring retarget (PR #93) found several correctness bugs beyond the ones already fixed on this branch: - new-agent.sh silently walked a marker-less subdirectory under $HOME up to user scope, contradicting its own usage text ("user scope is checked directly, no walk-up") and risking scaffolding into shared global ~/.claude or ~/.copilot directories instead of the intended local path. - The hand-copied apm.yml type: manifest detector in new-agent.sh and new-skill.sh accepted mismatched quotes (e.g. `type: "skill'`) that validate.sh's regex correctly rejects, and silently dropped a final apm.yml line lacking a trailing newline — causing the scaffolder and validator to disagree on scope for identical input. - Plugin-scope agent frontmatter could still contain the apm-agent.md template's HTML comments at ship time with no audit signal, yet apm compile copies frontmatter verbatim and <!-- --> breaks YAML parsing on both downstream harnesses. - ADR-0016 asserted agent-audit already implements a SUGGESTION heuristic for tool-restriction-needing plugin-scope agents; it doesn't. - agent-audit/README.md still described the old plugin-pair model this PR replaced with a single-file allowlist model. - validate.sh's project/user-scope CC-only/Copilot-only field checks and counterpart-missing check lost their only test coverage when the old plugin-pair fixture was deleted. Also replaces an echo-into-sed two-value parse (4 forks per call) with a single space-separated echo + read in both scaffolders. Regression tests added for every fix above, including one for a bug this pass introduced and the test suite caught: an initial two-line echo + `read` attempt silently dropped the second value, since `read` consumes only one line regardless of embedded newlines. Full suite: 158 bats tests, 39 shell-script tests, 12/12 summary categories, 0 failures. Refs: #89, #93
This commit is contained in:
@@ -82,21 +82,24 @@ if [[ ! -d "$TARGET_INPUT" ]]; then
|
||||
fi
|
||||
|
||||
# True if apm_yml's top-level `type:` line names one of the four APM package
|
||||
# types (instructions/skill/hybrid/prompts) — tolerating an optional matching
|
||||
# quote around the value and requiring the value end there, so a malformed
|
||||
# value like `prompts-only` doesn't false-match on the `prompts` prefix.
|
||||
# types (instructions/skill/hybrid/prompts) — mirrors validate.sh's
|
||||
# APM_TYPE_RE: an optional quote around the value must be closed by the
|
||||
# *same* quote character (a mismatched or unterminated quote is rejected,
|
||||
# not silently stripped), and the value must be followed by whitespace or
|
||||
# end-of-line so `prompts-only` doesn't false-match on the `prompts` prefix.
|
||||
# `|| [[ -n "$line" ]]` in the read condition also processes a final line
|
||||
# that lacks a trailing newline, which `read` alone would otherwise skip.
|
||||
# Identical to agent-author's new-agent.sh copy of this helper.
|
||||
is_apm_package_manifest() {
|
||||
local apm_yml="$1" line value
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^type:[[:space:]]*(.*)$ ]] || continue
|
||||
value="${BASH_REMATCH[1]}"
|
||||
value="${value%%[[:space:]]*}"
|
||||
value="${value#\"}"; value="${value%\"}"
|
||||
value="${value#\'}"; value="${value%\'}"
|
||||
case "$value" in
|
||||
instructions|skill|hybrid|prompts) return 0 ;;
|
||||
esac
|
||||
local apm_yml="$1" line
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^type:[[:space:]]*(instructions|skill|hybrid|prompts)([[:space:]]|$) ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$line" =~ ^type:[[:space:]]*([\"\'])(instructions|skill|hybrid|prompts)([\"\'])([[:space:]]|$) ]] \
|
||||
&& [[ "${BASH_REMATCH[1]}" == "${BASH_REMATCH[3]}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < "$apm_yml"
|
||||
return 1
|
||||
}
|
||||
@@ -105,7 +108,7 @@ is_apm_package_manifest() {
|
||||
# Walk up from <path> looking for a type-bearing apm.yml (package mode) or a
|
||||
# .git boundary / filesystem root (standalone mode). An apm.yml with no
|
||||
# top-level 'type:' field is a marketplace-only manifest — skip it and keep
|
||||
# walking up. Prints two lines: the resolved root, then the mode.
|
||||
# walking up. Prints one space-separated line: mode, then the resolved root.
|
||||
# ---------------------------------------------------------------------------
|
||||
find_package_root() {
|
||||
local current
|
||||
@@ -113,8 +116,7 @@ find_package_root() {
|
||||
while true; do
|
||||
if [[ -f "$current/apm.yml" ]]; then
|
||||
if is_apm_package_manifest "$current/apm.yml"; then
|
||||
echo "$current"
|
||||
echo "package"
|
||||
echo "package $current"
|
||||
return 0
|
||||
fi
|
||||
# apm.yml exists but has no type: field — marketplace-only manifest.
|
||||
@@ -123,15 +125,13 @@ find_package_root() {
|
||||
# .git is a directory in a normal checkout but a file (`gitdir: ...`) in
|
||||
# a git worktree — -e covers both.
|
||||
if [[ -e "$current/.git" ]]; then
|
||||
echo "$current"
|
||||
echo "no-package"
|
||||
echo "no-package $current"
|
||||
return 0
|
||||
fi
|
||||
local parent
|
||||
parent="$(dirname "$current")"
|
||||
if [[ "$parent" == "$current" ]]; then
|
||||
echo "$current"
|
||||
echo "no-package"
|
||||
echo "no-package $current"
|
||||
return 0
|
||||
fi
|
||||
current="$parent"
|
||||
@@ -139,10 +139,12 @@ find_package_root() {
|
||||
}
|
||||
|
||||
# `mapfile`/`readarray` are bash 4.0+ builtins with no fallback on macOS's
|
||||
# stock /bin/bash 3.2 — read the two output lines individually instead.
|
||||
# stock /bin/bash 3.2 — read the single space-separated output line with a
|
||||
# plain `read` instead (bash 3.2-safe). `read` consumes only one line, so
|
||||
# mode and path must be on the same line: MODE first (never contains
|
||||
# whitespace), PKG_ROOT last (safely absorbs a path containing spaces).
|
||||
WALK_OUTPUT="$(find_package_root "$TARGET_INPUT")"
|
||||
PKG_ROOT="$(echo "$WALK_OUTPUT" | sed -n '1p')"
|
||||
MODE="$(echo "$WALK_OUTPUT" | sed -n '2p')"
|
||||
read -r MODE PKG_ROOT <<< "$WALK_OUTPUT"
|
||||
|
||||
if [[ "$MODE" == "package" ]]; then
|
||||
TARGET="$PKG_ROOT/.apm/skills/$SKILL_NAME"
|
||||
|
||||
Reference in New Issue
Block a user