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/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adept",
"version": "2.12.3",
"version": "2.12.4",
"description": "Development-workflow skills: design, TDD, adversarial review, shipping, and campaign orchestration for Claude Code and Codex.",
"author": {
"name": "David Christensen"
Expand Down
44 changes: 43 additions & 1 deletion .github/scripts/check-records-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,48 @@ STUB
run_case "gate file witness faults, not silently unprotected" 1 E-GATE-SCAN "$d" \
BASE_SHA="$b" PATH="$stub_bin:$PATH"

# A checker basename is repository-controlled and may look like a git-grep option. Quoting
# keeps it one shell argument; only `-e` keeps Git from parsing it as an option and silently
# omitting the workflow that protects the renamed checker.
d="$SCRATCH/option_shaped_checker"
new_repo "$d"
mv "$d/.github/scripts/check-records.sh" "$d/.github/scripts/--cached"
sed 's/check-records\.sh/--cached/' "$d/.github/workflows/records.yml" >"$d/.workflow"
mv "$d/.workflow" "$d/.github/workflows/records.yml"
write_record "$d" "0001-valid.md"
git -C "$d" add -A
git -C "$d" commit -qm base
b=$(base_of "$d")
rm "$d/.github/workflows/records.yml"
cat >"$d/.github/scripts/check-records.sh" <<'STUB'
#!/usr/bin/env bash
exec "$(dirname "$0")/--cached" "$@"
STUB
chmod +x "$d/.github/scripts/check-records.sh"
run_case "option-shaped checker still protects its workflow" 1 E-GATE-GONE "$d" \
BASE_SHA="$b"

# A workflow search that could not read the base ref must not silently shrink the gate's
# protected path set. The checker itself and its suite still give gate_paths two paths, so
# this fixture would otherwise pass while omitting the workflow whose scan faulted.
stub_bin="$SCRATCH/git-gate-paths-fault-bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/git" <<STUB
#!/usr/bin/env bash
if [ "\$1" = grep ] && [ "\$3" = -lF ]; then
for arg in "\$@"; do
if [ "\$arg" = .github/workflows ]; then
printf 'fatal: fixture-fault: simulated object store error\n' >&2
exit 128
fi
done
fi
exec "$real_git" "\$@"
STUB
chmod +x "$stub_bin/git"
run_case "gate workflow search faults, not silently omitted" 1 E-GATE-PATHS-SCAN "$d" \
BASE_SHA="$b" PATH="$stub_bin:$PATH"

# A base ref that predates the gate is the adoption PR, and it must not be red — and it
# must say why, per the same discipline as every other rule: assert both the exit status
# and which code fired. Bespoke rather than run_case: I-GATE-BOOTSTRAP is informational,
Expand Down Expand Up @@ -1327,7 +1369,7 @@ STUB
mkdir -p "$stub_bin"
cat >"$stub_bin/git" <<STUB
#!/usr/bin/env bash
if [ "\$1" = grep ]; then
if [ "\$1" = grep ] && [ "\$3" = -qF ]; then
for arg in "\$@"; do
if [ "\$arg" = .github/workflows ]; then
printf 'fatal: fixture-fault: simulated object store error\n' >&2
Expand Down
77 changes: 46 additions & 31 deletions .github/scripts/check-records.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1159,7 +1159,13 @@ gate_existed_at() {
# profile — a repo with more than one profile would otherwise print every gate finding once
# per profile in RECORD_PROFILES.
check_gate_files() {
local base=$1 self successor successor_path gate_path_count=0 self_status still_status succ_status
local base=$1 paths paths_status=0 self successor successor_path gate_path_count=0
local self_status still_status succ_status
paths=$(gate_paths "$base") || paths_status=$?
if [ "$paths_status" -ne 0 ]; then
err_full "E-GATE-PATHS-SCAN: $base: could not search the base ref's workflows for gate paths (git exit $paths_status)"
return
fi
while IFS= read -r self; do
[ -n "$self" ] || continue
self_status=0
Expand Down Expand Up @@ -1210,7 +1216,7 @@ check_gate_files() {
1) err "E-GATE-GONE: $self was deleted or untracked — the gate cannot be removed by the change it gates" ;;
*) err_full "E-GATE-SUCCESSOR-SCAN: $successor_path: could not read the index entry for $self's declared successor, so the rename is unverified (git exit $tracked_in_index_status)" ;;
esac
done < <(gate_paths "$base" | sort -u)
done < <(printf '%s\n' "$paths" | sort -u)

# An empty protected set means self-protection is off. That is the correct state for the
# PR that installs the gate in a new repo, and a silent failure for one that renamed it —
Expand Down Expand Up @@ -1249,11 +1255,11 @@ repo_relative() {
# stopping it from mentioning the checker — swapping it for a symlink to an inert file does
# exactly that. A repo driving the checker from something other than GitHub Actions yields
# no workflow here, which is not an error; that file is just not protected.
# Emits paths only. It must not call err: callers read it through a process substitution,
# where an err would set failed=1 in a subshell and lose it — the same shape that once let a
# stray file print an error and still exit 0. The caller checks locatability itself.
# Emits paths only. It must not call err: callers capture its output and status separately,
# because an err inside that subshell would set failed=1 only in the discarded process. The
# caller reports a non-zero status before it reads any paths.
gate_paths() {
local base=$1 rel profile old key new needle profiles_rel
local base=$1 rel profile old key new needle profiles_rel matches grep_status needles
rel=$(repo_relative "$SELF_FILE")
[ -n "$rel" ] && printf '%s\n' "$rel"

Expand Down Expand Up @@ -1296,11 +1302,10 @@ gate_paths() {
# it and left every profile silently unprotected. A directory merely absent from the ref
# exits 0 with no output, so that case never needed the guard.
#
# The `|| true` stays. gate_paths runs inside a process substitution, where err's
# assignment to `failed` lands in a discarded subshell -- it emits paths only, and its
# callers report. A damaged object store is therefore an accepted residual here rather than
# an oversight; giving this one function a fault channel means a private sentinel protocol
# between it and its caller, which ADR 0005 weighed and rejected.
# The `|| true` stays. The caller now captures gate_paths' status, but this listing still
# runs behind the process substitution feeding the loop, so its status cannot reach the
# function's return. Closing that residual means moving profile enumeration out of this
# shape; issue #89 scopes the workflow search below, not this listing.
profiles_rel=$(repo_relative "$SELF_DIR/profiles")
if [ -n "$profiles_rel" ]; then
while IFS= read -r profile; do
Expand Down Expand Up @@ -1332,26 +1337,36 @@ gate_paths() {
# are generic enough to appear in an unrelated repo's workflow for reasons that have nothing
# to do with this gate.
#
# `sort -u` at the end: a workflow naming more than one needle (this repo's own workflow
# names both check-debt.sh and check-debt-test.sh in the same PR) would otherwise be
# emitted once per match, and check_gate_files would then report the same path's finding
# more than once.
{
printf '%s\n' "$(basename "$SELF_FILE")"
while IFS= read -r old; do
[ -n "$old" ] || continue
key=${old%%$'\t'*}
new=${old#*$'\t'}
key=${key##*/}
new=${new##*/}
case "$key" in
*.sh) [ "$key" = "$new" ] || printf '%s\n' "$key" ;;
esac
done <<<"$GATE_PREDECESSORS"
} | while IFS= read -r needle; do
git grep --no-color -lF "$needle" "$base" -- .github/workflows 2>/dev/null |
sed 's/^[^:]*://' || true
done | sort -u
# The caller sorts the complete result: a workflow naming more than one needle (this repo's
# own workflow names both check-debt.sh and check-debt-test.sh in the same PR) would otherwise
# be emitted once per match, and check_gate_files would report the same finding more than once.
needles=${SELF_FILE##*/}
while IFS= read -r old; do
[ -n "$old" ] || continue
key=${old%%$'\t'*}
new=${old#*$'\t'}
key=${key##*/}
new=${new##*/}
case "$key" in
*.sh) [ "$key" = "$new" ] || needles="$needles
$key" ;;
esac
done <<<"$GATE_PREDECESSORS"

while IFS= read -r needle; do
grep_status=0
matches=$(git grep --no-color -lF -e "$needle" "$base" -- .github/workflows 2>/dev/null) ||
grep_status=$?
case $grep_status in
0)
while IFS= read -r rel; do
printf '%s\n' "${rel#*:}"
done <<<"$matches"
;;
1) ;;
*) return "$grep_status" ;;
esac
done <<<"$needles"
}

# `git rev-parse --show-toplevel` exits non-zero for more than "you are not inside a git
Expand Down
44 changes: 43 additions & 1 deletion skills/tome-of-lore/assets/check-records-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,48 @@ STUB
run_case "gate file witness faults, not silently unprotected" 1 E-GATE-SCAN "$d" \
BASE_SHA="$b" PATH="$stub_bin:$PATH"

# A checker basename is repository-controlled and may look like a git-grep option. Quoting
# keeps it one shell argument; only `-e` keeps Git from parsing it as an option and silently
# omitting the workflow that protects the renamed checker.
d="$SCRATCH/option_shaped_checker"
new_repo "$d"
mv "$d/.github/scripts/check-records.sh" "$d/.github/scripts/--cached"
sed 's/check-records\.sh/--cached/' "$d/.github/workflows/records.yml" >"$d/.workflow"
mv "$d/.workflow" "$d/.github/workflows/records.yml"
write_record "$d" "0001-valid.md"
git -C "$d" add -A
git -C "$d" commit -qm base
b=$(base_of "$d")
rm "$d/.github/workflows/records.yml"
cat >"$d/.github/scripts/check-records.sh" <<'STUB'
#!/usr/bin/env bash
exec "$(dirname "$0")/--cached" "$@"
STUB
chmod +x "$d/.github/scripts/check-records.sh"
run_case "option-shaped checker still protects its workflow" 1 E-GATE-GONE "$d" \
BASE_SHA="$b"

# A workflow search that could not read the base ref must not silently shrink the gate's
# protected path set. The checker itself and its suite still give gate_paths two paths, so
# this fixture would otherwise pass while omitting the workflow whose scan faulted.
stub_bin="$SCRATCH/git-gate-paths-fault-bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/git" <<STUB
#!/usr/bin/env bash
if [ "\$1" = grep ] && [ "\$3" = -lF ]; then
for arg in "\$@"; do
if [ "\$arg" = .github/workflows ]; then
printf 'fatal: fixture-fault: simulated object store error\n' >&2
exit 128
fi
done
fi
exec "$real_git" "\$@"
STUB
chmod +x "$stub_bin/git"
run_case "gate workflow search faults, not silently omitted" 1 E-GATE-PATHS-SCAN "$d" \
BASE_SHA="$b" PATH="$stub_bin:$PATH"

# A base ref that predates the gate is the adoption PR, and it must not be red — and it
# must say why, per the same discipline as every other rule: assert both the exit status
# and which code fired. Bespoke rather than run_case: I-GATE-BOOTSTRAP is informational,
Expand Down Expand Up @@ -1327,7 +1369,7 @@ STUB
mkdir -p "$stub_bin"
cat >"$stub_bin/git" <<STUB
#!/usr/bin/env bash
if [ "\$1" = grep ]; then
if [ "\$1" = grep ] && [ "\$3" = -qF ]; then
for arg in "\$@"; do
if [ "\$arg" = .github/workflows ]; then
printf 'fatal: fixture-fault: simulated object store error\n' >&2
Expand Down
77 changes: 46 additions & 31 deletions skills/tome-of-lore/assets/check-records.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1159,7 +1159,13 @@ gate_existed_at() {
# profile — a repo with more than one profile would otherwise print every gate finding once
# per profile in RECORD_PROFILES.
check_gate_files() {
local base=$1 self successor successor_path gate_path_count=0 self_status still_status succ_status
local base=$1 paths paths_status=0 self successor successor_path gate_path_count=0
local self_status still_status succ_status
paths=$(gate_paths "$base") || paths_status=$?
if [ "$paths_status" -ne 0 ]; then
err_full "E-GATE-PATHS-SCAN: $base: could not search the base ref's workflows for gate paths (git exit $paths_status)"
return
fi
while IFS= read -r self; do
[ -n "$self" ] || continue
self_status=0
Expand Down Expand Up @@ -1210,7 +1216,7 @@ check_gate_files() {
1) err "E-GATE-GONE: $self was deleted or untracked — the gate cannot be removed by the change it gates" ;;
*) err_full "E-GATE-SUCCESSOR-SCAN: $successor_path: could not read the index entry for $self's declared successor, so the rename is unverified (git exit $tracked_in_index_status)" ;;
esac
done < <(gate_paths "$base" | sort -u)
done < <(printf '%s\n' "$paths" | sort -u)

# An empty protected set means self-protection is off. That is the correct state for the
# PR that installs the gate in a new repo, and a silent failure for one that renamed it —
Expand Down Expand Up @@ -1249,11 +1255,11 @@ repo_relative() {
# stopping it from mentioning the checker — swapping it for a symlink to an inert file does
# exactly that. A repo driving the checker from something other than GitHub Actions yields
# no workflow here, which is not an error; that file is just not protected.
# Emits paths only. It must not call err: callers read it through a process substitution,
# where an err would set failed=1 in a subshell and lose it — the same shape that once let a
# stray file print an error and still exit 0. The caller checks locatability itself.
# Emits paths only. It must not call err: callers capture its output and status separately,
# because an err inside that subshell would set failed=1 only in the discarded process. The
# caller reports a non-zero status before it reads any paths.
gate_paths() {
local base=$1 rel profile old key new needle profiles_rel
local base=$1 rel profile old key new needle profiles_rel matches grep_status needles
rel=$(repo_relative "$SELF_FILE")
[ -n "$rel" ] && printf '%s\n' "$rel"

Expand Down Expand Up @@ -1296,11 +1302,10 @@ gate_paths() {
# it and left every profile silently unprotected. A directory merely absent from the ref
# exits 0 with no output, so that case never needed the guard.
#
# The `|| true` stays. gate_paths runs inside a process substitution, where err's
# assignment to `failed` lands in a discarded subshell -- it emits paths only, and its
# callers report. A damaged object store is therefore an accepted residual here rather than
# an oversight; giving this one function a fault channel means a private sentinel protocol
# between it and its caller, which ADR 0005 weighed and rejected.
# The `|| true` stays. The caller now captures gate_paths' status, but this listing still
# runs behind the process substitution feeding the loop, so its status cannot reach the
# function's return. Closing that residual means moving profile enumeration out of this
# shape; issue #89 scopes the workflow search below, not this listing.
profiles_rel=$(repo_relative "$SELF_DIR/profiles")
if [ -n "$profiles_rel" ]; then
while IFS= read -r profile; do
Expand Down Expand Up @@ -1332,26 +1337,36 @@ gate_paths() {
# are generic enough to appear in an unrelated repo's workflow for reasons that have nothing
# to do with this gate.
#
# `sort -u` at the end: a workflow naming more than one needle (this repo's own workflow
# names both check-debt.sh and check-debt-test.sh in the same PR) would otherwise be
# emitted once per match, and check_gate_files would then report the same path's finding
# more than once.
{
printf '%s\n' "$(basename "$SELF_FILE")"
while IFS= read -r old; do
[ -n "$old" ] || continue
key=${old%%$'\t'*}
new=${old#*$'\t'}
key=${key##*/}
new=${new##*/}
case "$key" in
*.sh) [ "$key" = "$new" ] || printf '%s\n' "$key" ;;
esac
done <<<"$GATE_PREDECESSORS"
} | while IFS= read -r needle; do
git grep --no-color -lF "$needle" "$base" -- .github/workflows 2>/dev/null |
sed 's/^[^:]*://' || true
done | sort -u
# The caller sorts the complete result: a workflow naming more than one needle (this repo's
# own workflow names both check-debt.sh and check-debt-test.sh in the same PR) would otherwise
# be emitted once per match, and check_gate_files would report the same finding more than once.
needles=${SELF_FILE##*/}
while IFS= read -r old; do
[ -n "$old" ] || continue
key=${old%%$'\t'*}
new=${old#*$'\t'}
key=${key##*/}
new=${new##*/}
case "$key" in
*.sh) [ "$key" = "$new" ] || needles="$needles
$key" ;;
esac
done <<<"$GATE_PREDECESSORS"

while IFS= read -r needle; do
grep_status=0
matches=$(git grep --no-color -lF -e "$needle" "$base" -- .github/workflows 2>/dev/null) ||
grep_status=$?
case $grep_status in
0)
while IFS= read -r rel; do
printf '%s\n' "${rel#*:}"
done <<<"$matches"
;;
1) ;;
*) return "$grep_status" ;;
esac
done <<<"$needles"
}

# `git rev-parse --show-toplevel` exits non-zero for more than "you are not inside a git
Expand Down
Loading