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.0",
"version": "2.12.1",
"description": "Development-workflow skills: design, TDD, adversarial review, shipping, and campaign orchestration for Claude Code and Codex.",
"author": {
"name": "David Christensen"
Expand Down
85 changes: 84 additions & 1 deletion .github/scripts/check-records-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,61 @@ STUB
expect_error_code "$d/.e3" E-GATE-WITNESS-SCAN
fi

# A later positive witness keeps the undeclared-rename verdict, but the earlier fault still
# needs a trace. The unrelated workflow names debt.sh, a successor whose basename did not
# change, so gate_paths does not treat that workflow as protected; gate_existed_at's broader
# closed witness set still sees it after the SELF_DIR probe for debt.sh faults.
d="$SCRATCH/gate_witness_outranks_fault"
mkdir -p "$d/docs/debt" "$d/.github/scripts/profiles" "$d/.github/workflows"
git -C "$d" init -q .
git -C "$d" config user.email test@example.invalid
git -C "$d" config user.name "check-records test"
cat >"$d/.github/workflows/unrelated.yml" <<'YAML'
name: unrelated
on: push
jobs:
note:
runs-on: ubuntu-latest
steps:
- run: echo debt.sh
YAML
git -C "$d" add -A
git -C "$d" commit -qm "before the gate"
b=$(base_of "$d")
cp "$SCRIPT_DIR/check-records.sh" "$d/.github/scripts/"
cp "$SCRIPT_DIR/profiles/debt.sh" "$d/.github/scripts/profiles/"
chmod +x "$d/.github/scripts/check-records.sh"
write_record "$d" "0001-first.md"
git -C "$d" add -A
stub_bin="$SCRATCH/git-witness-outrank-bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/git" <<STUB
#!/usr/bin/env bash
if [ "\$1" = ls-tree ]; then
for arg in "\$@"; do
if [ "\$arg" = .github/scripts/debt.sh ]; 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 "a real gate witness outranks an earlier fault" 1 E-GATE-EMPTY-SET "$d" \
BASE_SHA="$b" PATH="$stub_bin:$PATH"
printf ' %-4s %-44s ' "" "the outranked gate fault is reported"
expect_match "$d/.err" 'W-GATE-WITNESS-SCAN beside the verdict' \
'the incomplete gate-witness search was silent' '::warning::W-GATE-WITNESS-SCAN: '
printf ' %-4s %-44s ' "" "the fault path and status are retained"
expect_match "$d/.err" 'gate fault path and status named' \
'the gate warning did not name the failed witness' \
'.github/scripts/debt.sh: .*git exit 128'
printf ' %-4s %-44s ' "" "the no-answer scan error stays suppressed"
expect_no_match "$d/.err" 'E-GATE-WITNESS-SCAN suppressed' \
'the positive witness was replaced by the no-answer fault' \
'::error::E-GATE-WITNESS-SCAN: '

# gate_existed_at has two witnesses, and `renamed_gate` above happens to satisfy both at
# once (its base-ref workflow names "check-records.sh", which is also the literal
# basename sitting in SELF_DIR at that ref) — so neither witness is individually proven.
Expand Down Expand Up @@ -2044,11 +2099,39 @@ STUB
b=$(base_of "$d")
git -C "$d" mv docs/debt/0005-b.md docs/debt/0006-b.md
write_record "$d" "0002-c.md"
write_record "$d" "0003-d.md"
git -C "$d" add -A
stub_bin="$SCRATCH/git-renumber-outrank-bin"
write_ls_files_stub "$stub_bin" docs/debt/0002-c.md
mkdir -p "$stub_bin"
real_git=$(command -v git)
cat >"$stub_bin/git" <<STUB
#!/usr/bin/env bash
if [ "\$1" = ls-files ]; then
for arg in "\$@"; do
case \$arg in
docs/debt/0002-c.md | docs/debt/0003-d.md)
printf 'fatal: fixture-fault: simulated index read error\n' >&2
exit 128
;;
esac
done
fi
exec "$real_git" "\$@"
STUB
chmod +x "$stub_bin/git"
run_case "a real renumber outranks a candidate fault" 0 - "$d" \
BASE_SHA="$b" PATH="$stub_bin:$PATH"
printf ' %-4s %-44s ' "" "the outranked fault is reported"
expect_match "$d/.err" 'exit=0 W-RENUMBER-SCAN' \
'the incomplete renumber search was silent' '::warning::W-RENUMBER-SCAN: '
printf ' %-4s %-44s ' "" "the last candidate fault is retained"
expect_match "$d/.err" 'last fault path and status named' \
'the warning did not name the last candidate fault' \
'could not read the index entry for docs/debt/0003-d.md, exit 128'
printf ' %-4s %-44s ' "" "the positive renumber action survives"
expect_match "$d/.out" 'renumber destination still reported' \
'the positive renumber result was lost' \
'note: docs/debt/0005-b.md was renumbered to docs/debt/0006-b.md'

# check_gate_files, on the gate file itself. A gate file present and tracked read as removed,
# which reported E-GATE-GONE -- the gate accusing the change of deleting a file that is
Expand Down
35 changes: 31 additions & 4 deletions .github/scripts/check-records.sh
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ $candidate"
else
used_renumber_targets=$candidate
fi
if [ "$fault_status" -ne 0 ]; then
path_exists_status=$fault_status
renumber_fault_path=$fault_path
return 3
fi
return 0
fi
done <<<"$records"
Expand Down Expand Up @@ -949,6 +954,10 @@ check_no_disappearances() {
renumbered_elsewhere "$base" "$record" || renum_status=$?
case $renum_status in
0) info "note: $record was renumbered to $renumbered_to (content unchanged)" ;;
3)
warn_full "W-RENUMBER-SCAN: $record: found renumber destination $renumbered_to after an incomplete search (could not read $renumber_fault_path, exit $path_exists_status)"
info "note: $record was renumbered to $renumbered_to (content unchanged)"
;;
1) err "E-GONE: $record is no longer a record at that path (deleted, moved, untracked, or renamed with its content changed) — resolve records in place with a '> **Resolved by ...**' banner" ;;
# Reported instead of E-GONE, never alongside it: whether the record moved is exactly what
# could not be established — by a candidate witness that did not run, by a candidate the
Expand Down Expand Up @@ -1074,7 +1083,8 @@ gate_known_basenames() {
# result is checked non-empty before use: unguarded, an empty result makes the git argument
# "${base}:/name", and depending on git's tree lookup an absolute-looking path can still
# resolve — checked explicitly rather than relied on to fail closed.
# Three-valued: 0 a witness found the gate, 1 no witness did, 2 a witness could not run.
# Four outcomes: 0 a witness found the gate after a complete search, 1 no witness did, 2 no
# witness answered because a scan faulted, 3 a witness found the gate after an earlier fault.
# Neither witness used to capture its status, so a scan fault was indistinguishable from every
# witness genuinely finding nothing — and the caller reported that as I-GATE-BOOTSTRAP, an
# exit-0 informational line, instead of the fatal E-GATE-EMPTY-SET an undeclared rename owes.
Expand All @@ -1093,7 +1103,13 @@ gate_existed_at() {
status=0
path_exists_at "$base" "${rel}/${name}" || status=$?
case $status in
0) return 0 ;;
0)
if [ "$fault_status" -ne 0 ]; then
path_exists_status=$fault_status
return 3
fi
return 0
;;
1) ;;
*)
fault_status=$path_exists_status
Expand All @@ -1110,7 +1126,13 @@ gate_existed_at() {
status=0
git grep --no-color -qF "$name" "$base" -- .github/workflows 2>/dev/null || status=$?
case $status in
0) return 0 ;;
0)
if [ "$fault_status" -ne 0 ]; then
path_exists_status=$fault_status
return 3
fi
return 0
;;
1) ;;
*)
fault_status=$status
Expand All @@ -1120,7 +1142,8 @@ gate_existed_at() {
done < <(gate_known_basenames)

# A positive witness returns above, so it outranks a fault on any other witness: evidence
# the gate existed is not weakened by an unrelated probe failing.
# the gate existed is not weakened by an unrelated probe failing. Return 3 rather than 0 in
# that case so the caller can report the incomplete search without changing its verdict.
if [ "$fault_status" -ne 0 ]; then
path_exists_status=$fault_status
return 2
Expand Down Expand Up @@ -1197,6 +1220,10 @@ check_gate_files() {
gate_existed_at "$base" || existed_status=$?
case $existed_status in
0) err "E-GATE-EMPTY-SET: no gate file from the base ref is protected — a rename must declare its predecessors in GATE_PREDECESSORS" ;;
3)
warn_full "W-GATE-WITNESS-SCAN: $gate_witness_path: a later witness established that a gate existed at $base after this read failed (git exit $path_exists_status)"
err "E-GATE-EMPTY-SET: no gate file from the base ref is protected — a rename must declare its predecessors in GATE_PREDECESSORS"
;;
1) info "I-GATE-BOOTSTRAP: no gate existed at $base — this is the change installing it" ;;
*) err_full "E-GATE-WITNESS-SCAN: $gate_witness_path: could not determine whether a gate existed at $base (git exit $path_exists_status)" ;;
esac
Expand Down
66 changes: 66 additions & 0 deletions docs/adr/0043-report-outranked-scan-faults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 0043 — Report outranked scan faults from the caller

## Status

Accepted (2026-08-27)

## Context

`renumbered_elsewhere` and `gate_existed_at` search several independent witnesses. A fault
from one candidate must not outrank a later positive result: a real renumber destination or
evidence that the gate existed remains valid. Both predicates therefore remember faults and
return success as soon as a positive result appears. That preserves the verdict but discards
the fact that part of the search did not run.

[ADR 0005](0005-scan-faults-are-reported-not-collapsed.md) requires a predicate to return a
distinct fault value and its caller to report it. Although ADR 0005 is formally superseded,
[ADR 0024](0024-a-failing-repository-probe-is-not-evidence-of-absence.md) withdraws one
consequence while carrying its three decisions forward unchanged. Those decisions do not define
an outcome that combines a positive answer with a reportable, outranked fault, and the existing
error channel would incorrectly turn an incomplete but decisive search into failure.

## Decision

An outranked scan fault is reportable. A multi-witness predicate that finds a positive result
after an earlier fault returns a distinct `positive-with-fault` status while preserving its
ordinary positive-result globals. Its caller emits one full-severity warning naming the failed
read and its exit status, then performs the same positive-result action it would have performed
for ordinary success.

When several reads fault before the positive result, the predicate retains and reports the last
fault encountered. The warning is a trace that the search was incomplete, not an inventory of
every failed read; earlier faults are not reported individually.

The two sites use rule-specific warning codes: `W-RENUMBER-SCAN` and
`W-GATE-WITNESS-SCAN`. A search with no positive result keeps ADR 0005's existing fault return
and error diagnostic. A search with no fault keeps its existing success or negative return.

## Consequences

The gate's exit status remains unchanged when a positive witness outranks a fault. Operators and
CI logs gain a stable trace showing that the answer was reached incompletely, with deterministic
selection when several reads fault. Callers acquire a fourth predicate outcome and must keep its
warning and positive action adjacent so later edits do not accidentally turn the warning into a
failing verdict or omit the positive action.

The warning is emitted with `warn_full`: an outranked external-read fault is independent of a
record's grandfathered shape and is not downgraded. The two mirrored gate scripts and their test
suites remain byte-identical.

## Considered & rejected

**Keep outranked faults silent.** judgment: observability; a correct verdict does not make an
external read that never ran irrelevant, and silence prevents an operator from distinguishing a
complete search from an incomplete one.

**Return the existing fault status and fail the gate.** verified: issue #87 identifies fixtures
where a later candidate or witness supplies a genuine positive result; treating the unrelated
fault as decisive would reverse the established verdict rather than report how it was reached.

**Emit the warning inside each predicate before returning success.** judgment: fit; it would
violate the caller-reporting boundary carried forward by ADR 0024 and make the predicate
responsible for both search and presentation.

**Add a side-channel global while continuing to return ordinary success.** judgment:
maintainability; every caller would have to remember to inspect optional state after an
indistinguishable success, recreating the silent omission this decision is meant to prevent.
120 changes: 120 additions & 0 deletions docs/workflow/plans/2026-08-27-report-outranked-scan-faults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Report outranked scan faults — implementation plan

**Issue:** #87

**Decision:** [ADR 0043](../../adr/0043-report-outranked-scan-faults.md)

**Spec:** [design](../specs/2026-08-27-report-outranked-scan-faults-design.md)

**Branch:** `feat/report-outranked-scan-faults-87`

**BASE_BRANCH:** `main`

## Global constraints

- Shell remains compatible with Bash 3.2, uses tab indentation, `#!/usr/bin/env bash`, and
`set -euo pipefail` where a file already carries them.
- Use only existing shell facilities and helpers; add no dependency or public API.
- `.github/scripts/check-records.sh` stays byte-identical to
`skills/tome-of-lore/assets/check-records.sh`; their `check-records-test.sh` suites also stay
byte-identical.
- Status `3` means `positive-with-fault`. It reports the last fault encountered, emits a warning,
and preserves the status-0 action and process verdict.
- Exact guardrails: focused `./.github/scripts/check-records-test.sh`; mirrored record gate
`just records`; full repository suite `just verify`. CI invokes the same suite through
`just ci`.
- Host architecture is `x86_64`; no target architecture is declared; relationship is
`no-target-declared`. The host uses GNU userland. Repository targets are not inferred from it.
- ADR index coupling is `not coupled`: `docs/adr/README.md` is deliberately a directory policy,
not an index table.

## Task 1 — add failing regression coverage

**Files:** modify `.github/scripts/check-records-test.sh`; mirror the exact file to
`skills/tome-of-lore/assets/check-records-test.sh` only after the focused test is red.

**Interfaces:** consumes existing helpers `write_ls_files_stub`, `run_case`, `expect_match`,
`expect_error_code`, and the `renumber_match_outranks_fault` fixture. Produces assertions for
`W-RENUMBER-SCAN`, `W-GATE-WITNESS-SCAN`, last-fault selection, and unchanged positive actions;
Task 2 must satisfy them without changing their expected codes.

1. Extend `renumber_match_outranks_fault` so two earlier candidate index reads fault with
distinguishable paths before `docs/debt/0006-b.md` matches. Keep expected exit `0`, then assert
`.err` contains `::warning::W-RENUMBER-SCAN:`, names the later faulting candidate and its exit,
and `.out` still names the real renumber destination. The path assertion specifically proves
the predicate copied its local `fault_path` into caller-visible `renumber_fault_path`.
2. Add a gate fixture whose protected set is empty, whose first two `ls-tree` witnesses fault with
distinguishable paths, and whose later workflow witness finds the former gate basename. Assert
exit `1`, `E-GATE-EMPTY-SET`, `W-GATE-WITNESS-SCAN` naming the second fault, and absence of
`E-GATE-WITNESS-SCAN`.
3. Run `./.github/scripts/check-records-test.sh` bare. Expected: non-zero, with the first new
warning assertion failing because neither caller emits the new code. This red result is the
TDD proof; do not alter production code before observing it.
4. Apply the same completed edits to the skill asset mirror and confirm
`cmp -s .github/scripts/check-records-test.sh skills/tome-of-lore/assets/check-records-test.sh`.

**Acceptance:** both fixtures reach a later positive witness after earlier injected faults; the
focused suite is demonstrably red for the missing warnings; both test mirrors are identical.

**Rollback:** revert only the two fixture edits; no scratch tree is retained by a completed suite.

## Task 2 — implement the fourth predicate outcome

**Files:** modify `.github/scripts/check-records.sh`; mirror it exactly to
`skills/tome-of-lore/assets/check-records.sh`.

**Interfaces:** consumes Task 1's exact warning codes and ADR 0043's status-3 contract. Preserves
`renumbered_to`, `renumber_fault_path`, `gate_witness_path`, and `path_exists_status` for callers.
Produces status `3` from `renumbered_elsewhere` and `gate_existed_at`; Task 3 relies on both script
mirrors being identical and the focused suite being green.

1. In each predicate's positive-result branch, return `3` when `fault_status` is non-zero and
return `0` otherwise. In `renumbered_elsewhere`, copy both local retained values before return:
`path_exists_status=$fault_status` and `renumber_fault_path=$fault_path`. In
`gate_existed_at`, copy `fault_status` to `path_exists_status`; its caller-visible
`gate_witness_path` already changes with every fault.
2. In the `renumbered_elsewhere` caller, add a status-3 case that emits
`warn_full "W-RENUMBER-SCAN: $record: found renumber destination $renumbered_to after an incomplete search (could not read $renumber_fault_path, exit $path_exists_status)"`
and then prints the same renumber note as status 0.
3. In the `gate_existed_at` caller, add a status-3 case that emits
`warn_full "W-GATE-WITNESS-SCAN: $gate_witness_path: a later witness established that a gate existed at $base after this read failed (git exit $path_exists_status)"`
and then emits the same `E-GATE-EMPTY-SET` error as status 0.
4. Apply the same production edits to its skill asset mirror. Run
`cmp -s .github/scripts/check-records.sh skills/tome-of-lore/assets/check-records.sh`.
5. Run `./.github/scripts/check-records-test.sh` bare. Expected: exit 0 and the suite's
`0 failed` summary.

**Acceptance:** status 3 preserves the positive verdict/action; one full-severity warning reports
the last retained fault; ordinary statuses 0, 1, and 2 retain their behavior; mirrors match.

**Rollback:** revert the caller and predicate cases together; a partial rollback would leave a
status value without a consumer or a consumer no predicate can reach.

## Task 3 — prove the tests bite and finish repository integration

**Files:** modify `.claude-plugin/plugin.json`; no other source file is expected.

**Interfaces:** consumes Task 2's green focused suite and identical mirrors. Produces installable
plugin version `2.12.1` and final guardrail evidence for delivery.

1. Temporarily neutralise only the `renumbered_elsewhere` status-3 caller warning. Run
`./.github/scripts/check-records-test.sh` bare and require non-zero with the new
`W-RENUMBER-SCAN` assertion failing. Restore the exact production line without committing the
mutation.
2. Temporarily neutralise only the `gate_existed_at` status-3 caller warning. Run
`./.github/scripts/check-records-test.sh` bare and require non-zero with the new
`W-GATE-WITNESS-SCAN` assertion failing. Restore the exact production line without committing
the mutation.
3. Run `./.github/scripts/check-records-test.sh` again. Expected: exit 0 with every suite assertion
passing and the summary reporting `0 failed`.
4. Change `.claude-plugin/plugin.json` version from `2.12.0` to `2.12.1`; this is a PATCH because
no invocation contract or capability is added.
5. Run `git diff --check`, then `just records`, then `just verify`, all bare. Expected: exit 0;
record mirrors compare byte-identically; all record tests pass; all repository gates and prek
checks pass without warnings.

**Acceptance:** both controlled faults turn the focused suite red; the restored implementation is
green; version `2.12.1` is parseable and greater than the base version; full guardrails pass.

**Rollback:** restore `2.12.0` only when reverting the entire branch. Never leave implementation
changes at the old version because installed plugin caches would not receive them.
Loading
Loading