Repository navigation
Harden the redaction transform; publish the playbook and seat wrapper from live - #2
Merged
Merged
Conversation
…ftest The fixer knew four pairs; the selftest's fixture tree built three and hashed two. So the "fixer writes nothing on a refusal" test exited at the fixer's missing-fourth-source check before it ever reached the write it exists to catch — a fixer planted to bypass the transform entirely passed 19/19. tests/pairs.sh is now the only list. refresh-from-live.sh and check-upstream-sync.sh iterate it, make_live materialises every row (and fails loudly on a row without a fixture), and the before/after digest covers every published file. The same plant now fails two cases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the deny-list, refuse unknown basenames
Fail-open holes, each with a red case in the selftest:
- Block rules required only their OPENING anchor. Rewording the closing line
("floor)." to "floor.)") left the awk skip running to end-of-file: exit 0,
five of fourteen lines emitted, steps 2-4 of the command gone, nothing said,
and the refresh would have published the gutted file. Every block now
asserts both ends (grep -E and awk share one ERE, passed via the environment
so backslashes survive), and the awk exits 1 if it reaches EOF still inside
the block or never entered it; a non-zero awk is a refusal naming the rule.
- The deny-list loader checked only -r. Empty, comments-only and indented-
comment-only lists passed everything; a CRLF-saved list gave every token a
trailing CR and matched nothing; stray whitespace did the same. The list is
now normalised (CR, surrounding whitespace, comments, blanks) and zero
surviving tokens is a refusal.
- Rules dispatched on the exact basename, so a renamed live file got none.
A basename outside tests/pairs.sh is refused; --no-rules is the explicit
override, and is itself refused on a file the table knows.
- Output is staged through temp files and emitted only after every rule and
the scan have passed.
All four published files still reproduce byte-for-byte from live.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rapper Five spots in the live files are facts about the author's machine and read as claims about the reader's: a dated private audit cited as precedent, a plugin "already wired into the harness", a Stop hook and a config file described as if they were the reader's, a private billing incident, and the same hook named in codex-seat.sh's header. Each becomes a block rule with both anchors asserted and a portable replacement (what a stranger's machine can do). Where a line carries the private name, the anchor stops short of it — as judge-precedent already does — so this script never spells what it removes. The selftest fixture uses invented names (FAKEHOOK, FAKEAPP, FAKELEDGER) that its deny fixture catches, so a rule that stops firing fails twice: once on the missing replacement, once on the token that reached the output. Every rule has a red case for each anchor. publicise-deny.example gains two placeholder rows for this class of token. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`publicise | diff` under POSIX sh has no pipefail: a refusal's exit 3 was lost and its stderr swallowed, so a REFUSING live file printed "STALE ... regenerate with refresh-from-live.sh" — the one command a refusal forbids, since the fixer would publish exactly what the transform refused. The transform now runs to a file; a non-zero exit is REFUSED with the transform's reason and the advice to fix the anchor or wording, never to refresh. Exit 1. A partial UNKNOWN (one to three live files absent) exited 0 and printed SYNC CLEAN over the files it did check. Any missing source is now SYNC UNKNOWN, exit 2, naming the absent files; REFUSED and STALE outrank it. The selftest now drives the gate against its fixture tree through the copy it already makes: CLEAN after a refresh over all 4 pairs, REFUSED without a "regenerate" line and without a duplicate STALE row, STALE on a hand-edited copy, UNKNOWN exit 2 with one source missing and with none. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The badge is hand-edited and the tag is typed; nothing tied them together. For each v* tag the README at that tag must carry the tag's version (red on a mismatch or a missing badge); HEAD gets an advisory when its badge runs ahead of the latest tag, which is the expected state on a release branch. Zero visible tags is UNKNOWN (exit 2), not a pass. --selftest builds a throwaway repo and proves all four verdicts. The README badge moves to 1.2.0: codex-seat.sh shipped as a feature after v1.1.2, so the next tag is a minor, and it follows the merge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eat wrapper Install sat at line 105 of 162 (65%); it now follows the opening pitch (line 31 of 186). The snippet gains the two lines a stranger needed and did not have: `mkdir -p ~/.claude/commands`, and copying scripts/codex-seat.sh into place with chmod +x — council.md calls that wrapper unconditionally, so the old snippet produced an empty Codex seat on every run. Requirements name bash, python3, sh, awk, diff and shasum. "What's in the box" gains rows for scripts/ and tests/. "Staying current" documents the four gate verdicts, REFUSED in particular, that UNKNOWN exits 2 whether one or all live sources are missing, the unknown-basename refusal, and check-release.sh. A workflow status badge joins the badge row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… gate says UNKNOWN ubuntu-latest: selftest-publicise.sh, codex-seat.sh --selftest, check-release.sh --selftest and check-release.sh (fetch-depth 0 so the tags are there to check), shellcheck -S warning over tests/ and scripts/, and a final step that runs check-upstream-sync.sh and requires exit 2 — the runner has no live ~/.claude, and a gate with nothing to check must say so rather than pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… guards Regression from the loader rewrite: the scan loop lost main's `|| [ -n "$token" ]`, and sed preserves a missing final newline, so a list whose only (or last) token had no trailing newline was COUNTED by the no-tokens gate and then never read by the scan — exit 0, the file published. Restored; two red cases (single token without newline, two tokens with the last unterminated). Three guards had no case that could go red — removing each left the selftest at 54/0: the case-insensitive deny match, the block-never-entered refusal (anchors present in the live file but swallowed by an earlier rule), and the line rule's grep-found-awk-missed refusal. One case each, proven by removing the guard: 58/1 every time, the right case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… checked against pairs.sh The gate printed "Fix the anchor or the live wording" for every non-zero publicise exit — including a missing or empty deny-list and an unknown basename, where that advice is wrong and nothing about the live file has been judged at all. publicise now tags the first stderr line of every refusal with its class (`publicise: [anchor|block|rules|unknown-file|deny-list|deny-hit] ...`) and check-upstream-sync.sh chooses its advice from that tag. Red: the selftest's gate case with no deny-list saw "Fix the anchor" against the previous gate. The `case` labels were a second list of basenames, untied to tests/pairs.sh: rename a row's live file and its rules go dead while the file stays pair_known and passes on the deny-list alone. The rules now live in one rules() table with an explicit `norules` marker for files published verbatim, and startup walks pairs.sh in dry mode refusing any row without an arm. Red: with the startup walk removed, a renamed row exits 0. Outputs are byte-identical to before the refactor for all four live files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live prose inserted between a block rule's opening and terminating anchors is replaced along with the block — no refusal, and it surfaces only as STALE at the next gate run. Said so in "Staying current", with the refresh diff as the place to catch it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rules() is invoked as `rules "$name" || fail`, which suspends `set -e` for its whole body, so advance()'s unguarded `mv -f "$nxt" "$cur"` could fail, drop that rule's output, and carry on from the previous $cur as if the rule had run. It now refuses with class [block] naming the rule. The gate's anchor|block advice arm had no assertion pinning it: deleting the arm left the selftest green because the generic fallback also says "do not refresh". Section 6 now requires the anchor-specific sentence on an anchor refusal; without the arm, 61/1 on exactly that case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`CDPATH= cd` becomes `CDPATH='' cd` in the five test scripts (SC1007). In refresh-from-live.sh the write loop's `dst` is assigned by the `eval` on the line above it, which shellcheck cannot follow; the SC2154 disable says so. `shellcheck -S warning tests/*.sh scripts/*.sh` now exits 0, which the CI step requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lines
A new section in the live playbook opened with a rule attributed to a named
person and closed by naming a Stop hook wired into the author's harness. No
rule covered either line and the deny-list had neither word, so the refresh
published both and the gate said CLEAN — the residual hole publicise.sh names
in its header (no anchor, no token). The deny-list now has the words, which
turned the leak into a [deny-hit] refusal; these two line rules make the file
publishable again.
playbook/fallback-owner replaces the attributed opening line with its portable
remainder; playbook/fallback-stop-hook replaces the hook line with what any
reader's hook can do. Each anchor runs from just after the private word to the
end of the line, so it never spells what it removes; the date is matched as a
shape (spelled out, no {4}, for mawk on the CI runner). The paragraph between
the two lines flows through untouched.
Selftest: fixture section with invented FAKEOWNER/FAKEHOOK (both in the deny
fixture), replacements and the untouched middle asserted, and each anchor
moved at its start side and its end side refuses naming the rule. Red before
the rules: 55 passed, 15 failed, led by the same [deny-hit] the real file hit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated with tests/refresh-from-live.sh; check-upstream-sync.sh now reads SYNC CLEAN for all four files. The playbook gains an "Availability fallback" section (when a review seat is down, the review step still runs with the strongest available Claude, labelled as a fallback), and the agy recipe now matches /council's flags. The seat wrapper judges an absent codex binary, a missing python3 and a malformed models cache as UNKNOWN with the reason, reads both streams into a variable instead of `cat | grep -q` (a SIGPIPE inverted the predicate on large output), and cleans its selftest temp dir on exit. Both files pass the redaction rules and the deny-list; the new section's two machine-specific lines are replaced by portable wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ason The selftest's "no cache" case now checks that --check says "no models cache", not only that it exits 2: a missing file also fails the malformed-cache parse, so the exit code alone passed with the no-cache branch deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens the redaction transform that turns the live commands into this public copy, and ships the published files it now produces. Release target: v1.2.0 (the badge already says so;
scripts/codex-seat.shis a feature, not a patch).The transform (
tests/publicise.sh) can no longer fail open--no-rulesis given, and the rule table is checked againsttests/pairs.shat startup.anchor,block,rules,unknown-file,deny-list,deny-hit) that the gate turns into the right advice.The gate (
tests/check-upstream-sync.sh) reports REFUSED separately from STALE, never advises a refresh after a refusal, and says UNKNOWN (exit 2) when any live source is missing.One pairs table (
tests/pairs.sh) is read by the transform, the gate, the fixer and the selftest.Release hygiene:
tests/check-release.shchecks that every tag's README badge equals the tag.README: Requirements and Install now sit in the first sixth of the page, and the Install snippet copies
codex-seat.sh, which/councilcalls.CI (
.github/workflows/selftests.yml): the redaction selftest, the seat selftest, the release check, shellcheck, and a step that requires the gate to say UNKNOWN on a runner with no live source.Evidence
sh tests/selftest-publicise.sh(sh and dash)main's transformsh tests/check-upstream-sync.shsh tests/check-release.shbash scripts/codex-seat.sh --selftest-S warningReview
The Codex seat was unavailable, so the branch went to a fresh Claude reader twice. It found an unterminated deny token that slipped past the loader, three guards no test could turn red, and a gate that gave the wrong advice for some refusals; all are fixed. At the refresh step a private owner name and a private hook name in a newly added live section reached the output while the gate read clean, because no rule and no deny token covered them yet. The output was discarded, the tokens were added to the machine-local deny list, and two rules now replace those lines.
After merge
tests/check-release.shthen flips its HEAD advisory to ok.🤖 Generated with Claude Code