diff --git a/.claude/rules/knowledge-base.md b/.claude/rules/knowledge-base.md index b459c69..d7b6154 100644 --- a/.claude/rules/knowledge-base.md +++ b/.claude/rules/knowledge-base.md @@ -86,6 +86,26 @@ valid, not correct. 2. **Describe the system, not the people.** Knowledge concentration is a property of the code. 3. **Keep negative results and contradictions.** +## The estate: what a program answers, a page does not + +- **A value the estate can compute is a check, not a sentence.** A deployed version, a certificate + expiry, a restart count, a quota, a licence date: write the probe into + `.claude/tools/status.project.sh` and cite the check. A date no system can answer (a vendor's + billing change, a renewal set by hand) goes into `.claude/deadlines.conf` with its lead time and + its owner. `status.sh` runs both at every `/orient`. Measured 2026-08-24 on one installation: + five of seven such sentences were stale or wrong within weeks of being written. +- **A runbook states its capabilities, not its author.** The header field `requires:` names tokens + from `00-orientation/capabilities.md`, one table row per token: a blast marker (馃煝 read-only or + non-production, 馃煛 changes non-production or reads production data, 馃敶 touches production), what + it grants, a read-only proof command, the expected output, and how to obtain it. `preflight.sh -c + ` proves them before the page is followed; `knowledge-check.sh` refuses a token the page + does not define. Where nobody has written how a capability is obtained, the row says UNRESOLVED + and names who to ask; an invented procedure is worse than an admitted gap. +- **Probes live beside the pages.** `_probes/` holds runnable, read-only scripts that re-measure a + claim; each opens with a `Requires:` line and ends with the traps that cost a run, and + `_probes/README.md` says what each one answers. An investigation cites the probe that produced its + numbers, so the number can be produced again. + ## `99-pending.md` One line, same turn, without asking. Grouped by who can act: only the project team 路 needs a diff --git a/.claude/rules/standing-orders.md b/.claude/rules/standing-orders.md index e7155f5..61c7bd9 100644 --- a/.claude/rules/standing-orders.md +++ b/.claude/rules/standing-orders.md @@ -29,7 +29,8 @@ Every piece of work gets a **tier**, stated in one line the owner can override: **The hard floor:** auth, authorization, payments, secrets, data migration, a public API or contract, a security control, cross-module architecture. Any of these is Tier 3 and cannot be -tiered down. Round up when unsure. +tiered down. Round up when unsure. A task whose runbook names, in its `requires:` field, a capability +marked as touching production is Tier 3 by that alone. After the yes, **build uninterrupted.** Pause only for the stop-list: a Tier-3 area touched unexpectedly 路 a new dependency 路 a schema or migration change 路 data deletion 路 anything reaching @@ -80,6 +81,11 @@ One more thing is mechanical, and it is a check rather than a hook: `verify.sh` base whose durable pages lack the required header, whose checked references do not resolve, or whose README states a count the tree contradicts. It decides structure, never whether a claim is true. +Two more things are checks rather than hooks. `status.sh` computes what the estate is doing right +now from the project's own read-only probes and its declared deadlines, and exits red on anything +RED; `/orient` runs it first and leads with those lines. `preflight.sh` proves, read-only, the +capabilities a page requires; `/work` runs it before the one yes. `verify.sh` proves both can go red. + ## Where things live | Path | What it is | diff --git a/.claude/rules/working-here.md b/.claude/rules/working-here.md index ca5ada3..9bc57cb 100644 --- a/.claude/rules/working-here.md +++ b/.claude/rules/working-here.md @@ -82,3 +82,29 @@ search for the same root cause. Same cause with a different symptom means merge, a file under `docs/knowledge-base/` left `knowledge-base.md` out of context for a whole turn; the `Read` tool on the same file brought it in at once. A session that prefers the shell writes knowledge-base pages without the rule that says how. Open one page with `Read` first. 2026-09-05. + +## Reasoning + +The traps above are about shells. These are about judgement, and they cost more. All five were +measured on production questions on one installation. + +- **The size of a mechanism reported as its effect.** A migration said to "revoke entitlements from + 353 subscriptions" read a collection holding zero documents; "86 of 88 messages dropped" counted + 52 correct skips and the system's own echoes; a dead-token alarm fired on channels where 1,083 + sends had just succeeded. Count the thing the customer has, not the thing the code touches; trace + one item end to end before trusting an aggregate; check whose. 2026-08-02 to 2026-08-08. +- **A green test on an unreachable path.** A fix recorded as proven by an offline replay (12 of 77 + cases to 44 of 77) had fired zero times in production: the replay supplied an input the running + system never produces. Ask of any passing suite which line would fail if the claim were false, + and after a deploy look for the success line the fix emits, not the failure line. 2026-08-24. +- **The dangerous zero.** Three zeros in one day meant "my question could not see it": a query that + had never run, a grep for a guard an upstream check made unreachable, and a health query reporting + zero healthy targets on a system that was visibly serving. Before believing a zero, confirm the + same query sees something you are certain exists. A zero that fits your hypothesis deserves more + scrutiny than one that contradicts it. 2026-08-24. +- **An instrument that measures itself.** A counter built to measure a behaviour scanned the very + text it was measuring and always agreed with itself. Validate a new instrument against a case + whose answer you already know before citing it. 2026-08-24. +- **Log volume read as usage.** A service with 26 log lines a day was written down as idle; it + carried all console traffic and logged at Warning. A quiet log is a mechanism; find the callers in + configuration and client code, which is the effect. 2026-09-20. diff --git a/.claude/rules/writing.md b/.claude/rules/writing.md index 71850cd..0653b76 100644 --- a/.claude/rules/writing.md +++ b/.claude/rules/writing.md @@ -12,8 +12,9 @@ reading effort, so simplify that part, never the terms. - **Lead with the bottom line.** One or two sentences that answer the question, then why it matters, then how it works. -- **One idea per sentence.** "Create the API endpoint", not "the implementation of the API endpoint - should be undertaken in order to facilitate the required functionality". +- **One idea per sentence, and most sentences under twenty words.** "Create the API endpoint", not + "the implementation of the API endpoint should be undertaken in order to facilitate the required + functionality". A sentence carrying two "which" clauses splits into two sentences. - **Plain words first.** A technical term is fine when it is the precise one. Define it in the same breath the first time it appears: "the drafter, which is the part that writes the database query". - **Name the real moving parts** and how they connect. An analogy supplements the mechanism; it @@ -38,16 +39,17 @@ reading effort, so simplify that part, never the terms. - **Absolute dates.** 2026-08-27, not "last Tuesday". When a time is needed, write it in UTC. - **One name per concept.** The project rule lists the project's terms; do not alternate. Where code and docs disagree, use the code's name and mention the other name once. +- **Numbers:** digits, not words; a unit on every quantity; "about" on every estimate; a base beside + every percentage ("5 of 7", not "71%"); and the date beside any number that can change, in chat + as much as in a page. +- **When the words are the record, quote them.** A decision, a waiver, or an instruction is written + verbatim, in quotation marks, with its date. A paraphrase of a decision is a different decision. ## Questions -Three things always get asked, and none of them are politeness: an instruction with two readings -(ask before acting, in its own message); a decision that is not yours to make, or whose options -have materially different consequences the project cannot settle; anything irreversible, or any -security, data-loss, or money decision. Everything else you work out yourself from the code, the -configuration, the knowledge base, and previous decisions. A question the tools could have answered -wastes a whole turn. When a question is necessary, make it decidable in one reply: one question, the -options named, a recommendation stated. +When to ask is settled in the standing orders (the three habits, the one stop, the stop-list); this +file only says how. A question the tools could have answered wastes a whole turn. When a question is +necessary, make it decidable in one reply: one question, the options named, a recommendation stated. ## Shape @@ -67,6 +69,12 @@ options named, a recommendation stated. items, tables for comparisons, numbered steps for a procedure. One purpose per section. - **Recommend one option.** "Recommended: X, because" beats a menu of five. Name an alternative only when it is genuinely close. +- **Bold one phrase per paragraph, or none:** the thing the reader must not miss. When everything is + bold, nothing is. +- **Markers are a fixed set:** the words RED, AMBER, OK and SKIP, and the blast markers 馃煝 馃煛 馃敶 with + the meanings the knowledge-base rule gives them. No decorative emoji, no ticks, no rockets. +- **Never paste more than about ten lines of output.** Quote the one line that matters, say what it + means, and say where the rest is. - **When the answer includes code:** use the architecture and conventions already in the project, verify what the code actually does before assuming anything, keep examples focused, and write no boilerplate nobody asked for. diff --git a/.claude/skills/orient/SKILL.md b/.claude/skills/orient/SKILL.md index 0b664e7..deb6f80 100644 --- a/.claude/skills/orient/SKILL.md +++ b/.claude/skills/orient/SKILL.md @@ -49,12 +49,19 @@ when the ref was last fetched. In a workspace, say which branch each clone is on branch is often not the trunk. Uncommitted work means somebody was mid-task. Say what is uncommitted and leave it. -## 4 路 What is actually deployed, when the project rule names a way to check +## 4 路 What the estate is doing, computed -Git does not know what is on the server. When the project rule names a deployed-state check (a -health endpoint that reports the running commit, a deploy tag, a file on the host), run it and -compare: `git log --oneline ..origin/` is what is not yet live. When there -is no such check, say so rather than guessing. +```bash +bash .claude/tools/status.sh +``` + +It runs the project's own read-only probes (`.claude/tools/status.project.sh`) and the declared +deadlines (`.claude/deadlines.conf`), and exits 1 on anything RED. **Lead the report with the RED +lines, each with its remedy:** a RED that stays inside the session is a RED nobody sees. Git does +not know what is on the server; when a probe reports the deployed commit, `git log --oneline +..origin/` is what is not yet live. When the project has no probes yet the +tool says so, and you say that rather than guessing. If a value you are about to state could be +read from a system, it belongs in `status.project.sh`, not in the report. ## 5 路 Does it still verify @@ -74,7 +81,7 @@ Five lines, no headings: 1. which branch, per clone in a workspace, and whether it is ahead of its upstream 2. where the last session got to, with its date 3. what is uncommitted or unfinished -4. what verify reported +4. what status and verify reported, RED first 5. **the one thing most worth doing next**, named. Not a list of five. Then stop and let the human choose. diff --git a/.claude/skills/work/SKILL.md b/.claude/skills/work/SKILL.md index a819316..7d76a62 100644 --- a/.claude/skills/work/SKILL.md +++ b/.claude/skills/work/SKILL.md @@ -73,6 +73,9 @@ A bottom line of what you are about to do, then the details, in the house writin - **the branch sentence:** which repository, cut from which branch after a fetch, following the project rule's branch model; which branch the pull request targets. When the repository does not follow the model, put the evidence in front of them and ask one question with the options named +- **the capabilities it needs, proven:** when a page the task follows names tokens in its `requires:` + field, `bash .claude/tools/preflight.sh -c ` has run before this message, and any FAIL or + UNRESOLVED is named here; a token marked as touching production makes the task Tier 3 - the tier line, and the route: Codex design review first, or go, with the default stated. Tier 3 review, Tier 2 offer it, Tier 1 go diff --git a/.claude/tools/estate.test.sh b/.claude/tools/estate.test.sh new file mode 100644 index 0000000..4e0d21c --- /dev/null +++ b/.claude/tools/estate.test.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# Runs status.sh and preflight.sh against fixture trees and asserts the exit code and the message. +# This is how the two estate tools prove they can go red: a gate that has never failed is not known +# to gate, and a failure with the wrong message is not actionable. +# +# bash .claude/tools/estate.test.sh +# +# Every case builds a real tree and runs the real tool with --root. Nothing here greps the tools' +# source, because a test that reads the script instead of running it passes over a broken script. + +unset MSYS_NO_PATHCONV +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ST="$HERE/status.sh"; PF="$HERE/preflight.sh" +[ -f "$ST" ] && [ -f "$PF" ] || { echo "status.sh or preflight.sh not found beside this suite"; exit 1; } +T="$(mktemp -d 2>/dev/null || mktemp -d -t estatetest)" +trap 'rm -rf "$T"' EXIT + +fails=0; total=0; got="" +check() { # name expected-exit needle + total=$((total+1)) + if [ "$got" != "$2" ]; then + printf ' FAIL %-58s expected exit %s got %s\n' "$1" "$2" "$got"; fails=$((fails+1)) + sed 's/^/ /' "$T/out" | head -8; return + fi + if [ -n "$3" ] && ! grep -qF -e "$3" "$T/out"; then + printf ' FAIL %-58s exit %s, no message matching: %s\n' "$1" "$got" "$3"; fails=$((fails+1)) + sed 's/^/ /' "$T/out" | head -8; return + fi + printf ' ok %-58s exit %s\n' "$1" "$got" +} +plus_days() { # n -> yyyy-mm-dd, GNU date first, BSD second + date -d "$1 days" +%Y-%m-%d 2>/dev/null || date -v"+$1d" +%Y-%m-%d +} +R="$T/p" +mk() { rm -rf "$R"; mkdir -p "$R/.claude/tools" "$R/docs/knowledge-base/00-orientation"; } +run_st() { got="$(bash "$ST" --root "$R" "$@" >"$T/out" 2>&1; echo $?)"; } +run_pf() { got="$(bash "$PF" --root "$R" "$@" >"$T/out" 2>&1; echo $?)"; } + +echo "status.sh: nothing to check" +mk; run_st; check "no probes and no deadlines is not a failure" 0 "no status.project.sh" +mk; run_st; check "it says the estate answers nothing yet" 0 "not checked." + +echo "status.sh: declared deadlines" +mk; printf '%s 30 owner :: a thing that is overdue :: some-page.md\n' "$(plus_days -3)" > "$R/.claude/deadlines.conf" +run_st; check "an overdue date is RED and the run fails" 1 "overdue by 3 d" +mk; printf '%s 30 owner :: within the warning window :: some-page.md\n' "$(plus_days 20)" > "$R/.claude/deadlines.conf" +run_st; check "a date inside warn_days is AMBER, run passes" 0 "-- AMBER (1) --" +mk; printf '%s 30 owner :: a red-zone date :: some-page.md\n' "$(plus_days 5)" > "$R/.claude/deadlines.conf" +run_st; check "a date inside a quarter of warn_days is RED" 1 "-- RED (1) --" +mk; printf '%s 30 owner :: far away :: some-page.md\n' "$(plus_days 200)" > "$R/.claude/deadlines.conf" +run_st; check "a far date is OK" 0 "-- OK (1) --" +mk; printf '%s 400 owner :: long runway, still near :: p.md\n' "$(plus_days 25)" > "$R/.claude/deadlines.conf" +run_st; check "the RED zone is capped at 21 days, so 25 days out is AMBER" 0 "-- AMBER (1) --" +mk; printf 'not-a-date 30 owner :: bad line :: p.md\n' > "$R/.claude/deadlines.conf" +run_st; check "a bad date is a RED about the file, not a silent skip" 1 "date is not yyyy-mm-dd" +mk; printf '%s 30 owner no separators at all\n' "$(plus_days 9)" > "$R/.claude/deadlines.conf" +run_st; check "a line without :: is a RED about the file" 1 "no '::' separator" +mk; printf '# only a comment\n\n' > "$R/.claude/deadlines.conf" +run_st; check "a file with no deadline lines is reported, not passed silently" 0 "has no deadline lines" + +echo "status.sh: the project's probes" +mk; printf '#!/usr/bin/env bash\nprintf "OK\\tpods\\tprod\\tall 12 running\\t\\n"\n' > "$R/.claude/tools/status.project.sh" +run_st; check "an OK finding passes and is shown" 0 "all 12 running" +mk; printf '#!/usr/bin/env bash\nprintf "RED\\tcerts\\t*.example.test\\texpires in 3 d\\trenew: run the cert job\\n"\n' > "$R/.claude/tools/status.project.sh" +run_st; check "a RED finding fails the run and prints its remedy" 1 "> renew: run the cert job" +mk; printf '#!/usr/bin/env bash\nprintf "RED\\tcerts\\tx\\tno remedy here\\t\\n"\n' > "$R/.claude/tools/status.project.sh" +run_st; check "a RED without a remedy is itself reported" 1 "RED finding without a remedy" +mk; printf '#!/usr/bin/env bash\necho "this is not a finding"\n' > "$R/.claude/tools/status.project.sh" +run_st; check "a malformed line is a RED, never dropped" 1 "malformed finding" +mk; printf '#!/usr/bin/env bash\nprintf "PURPLE\\ta\\tb\\tc\\td\\n"\n' > "$R/.claude/tools/status.project.sh" +run_st; check "an unknown level is malformed" 1 "malformed finding" +mk; printf '#!/usr/bin/env bash\nexit 3\n' > "$R/.claude/tools/status.project.sh" +run_st; check "a probe script that exits non-zero is RED" 1 "exited 3" +mk; printf '#!/usr/bin/env bash\n:\n' > "$R/.claude/tools/status.project.sh" +run_st; check "a probe that prints nothing is reported, not passed" 0 "printed no findings" +mk; printf '#!/usr/bin/env bash\nprintf "SKIP\\tlicence\\tpilot\\tno kubectl context\\t\\n"\n' > "$R/.claude/tools/status.project.sh" +run_st; check "SKIP is allowed without a remedy" 0 "no kubectl context" + +echo "status.sh: --check and --canary" +mk; printf '#!/usr/bin/env bash\nif then fi\n' > "$R/.claude/tools/status.project.sh" +run_st --check; check "--check catches a probe script that does not parse" 1 "does not parse" +mk; printf '#!/usr/bin/env bash\nprintf "RED\\ta\\tb\\tc\\td\\n"\n' > "$R/.claude/tools/status.project.sh" +run_st --check; check "--check runs no probe, so a RED probe does not fire" 0 "parses (not run)" +mk; run_st --canary; check "--canary fails and says both injections were seen" 1 "a synthetic RED and a malformed line were both reported" + +echo "preflight.sh" +cap="$R/docs/knowledge-base/00-orientation/capabilities.md" +mkcap() { + mk + cat > "$cap" <<'EOF' +--- +title: fixture capabilities +status: verified +as_of: 2026-09-22 +last_verified: 2026-09-22 +verification_method: estate.test.sh +scope: fixture +confidence: High. fixture +known_gaps: none +reverify_when: never +--- + +| Token | Blast | Grants | Proof (read-only) | Expected | Obtain | +|---|---|---|---|---|---| +| `TOOL-TRUE` | 馃煝 | a command that exits 0 | `true` | | install nothing | +| `TOOL-FALSE` | 馃煝 | a command that exits 1 | `false` | | ask nobody | +| `TOOL-MATCH` | 馃煝 | output matches | `echo hello world` | ^hello | none | +| `TOOL-NOMATCH` | 馃煛 | output does not match | `echo hello world` | ^goodbye | the docs | +| `TOOL-PIPE` | 馃煝 | a pipe inside the proof | `printf 'a\nb\n' \| wc -l` | 2 | none | +| `GAP-ONE` | 馃敶 | nobody knows how | `false` | UNRESOLVED | the platform owner | +EOF +} +mkcap; run_pf --list; check "--list runs nothing and exits 0" 0 "TOOL-FALSE" +mkcap; run_pf -c TOOL-TRUE; check "a passing proof" 0 "PASS TOOL-TRUE" +mkcap; run_pf -c TOOL-FALSE; check "a failing proof fails the run" 1 "FAIL TOOL-FALSE" +mkcap; run_pf -c TOOL-MATCH; check "expected regex matched" 0 "PASS TOOL-MATCH" +mkcap; run_pf -c TOOL-NOMATCH; check "expected regex not matched fails" 1 "output did not match" +mkcap; run_pf -c TOOL-PIPE; check "an escaped pipe inside the proof works" 0 "PASS TOOL-PIPE" +mkcap; run_pf -c GAP-ONE; check "UNRESOLVED is reported and does not fail" 0 "UNRESOLVED" +mkcap; run_pf -c NOPE-X; check "a token the page does not define fails" 1 "not defined" +mkcap; run_pf -c TOOL-TRUE,TOOL-FALSE; check "one failing token fails the set" 1 "1 failed" +mk; run_pf; check "no capabilities page is exit 2, not a pass" 2 "no capabilities page" +mkcap; printf '# nothing here\n' > "$cap"; run_pf; check "a page with no token rows is exit 2" 2 "no token rows" + +echo +if [ "$fails" -eq 0 ]; then echo " $total cases, 0 failed"; else echo " $total cases, $fails FAILED"; fi +[ "$fails" -eq 0 ] diff --git a/.claude/tools/knowledge-check.sh b/.claude/tools/knowledge-check.sh index 191f5dd..a0ef2b1 100644 --- a/.claude/tools/knowledge-check.sh +++ b/.claude/tools/knowledge-check.sh @@ -234,6 +234,21 @@ fence { next } } }' +# The header field `requires:` names capability tokens. Each must be a row on the capabilities page, +# where a token carries a read-only proof; a token nobody defined is a runbook nobody can check. +req_awk=' +NR == 1 { if ($0 !~ /^---[ \t\r]*$/) exit; next } +/^---[ \t\r]*$/ { exit } +/^requires:/ { + line = $0; sub(/^requires:/, "", line) + while (match(line, /[A-Z][A-Z0-9-]+/)) { + tok = substr(line, RSTART, RLENGTH) + if (length(tok) >= 3) print "requires\t" tok + line = substr(line, RSTART + RLENGTH) + } +}' +cappage=""; [ -f "$kb/00-orientation/capabilities.md" ] && cappage="$kb/00-orientation/capabilities.md" + # The defined decision codes, gathered once. An empty set means the pattern is broken, not that the # page has no decisions, so it fails rather than letting every citation pass. defined="" @@ -259,6 +274,7 @@ while IFS= read -r page; do [ -n "$page" ] || continue dir="${page%/*}" awk "$refs_awk" "$page" > "$TMP/refs" + awk "$req_awk" "$page" >> "$TMP/refs" while IFS=$'\t' read -r kind value; do [ -n "$value" ] || continue # A citation holding < > or * is a template, in every form: angle brackets are this base's @@ -293,6 +309,14 @@ while IFS= read -r page; do done [ "$found" = 1 ] || fail references "$page" "referenced commit does not resolve: $value" ;; + requires) + s_references=$((s_references+1)) + if [ -z "$cappage" ]; then + fail references "$page" "requires $value, but there is no capabilities page at 00-orientation/capabilities.md" + elif ! grep -qE "^\| *\`$value\` *\|" "$cappage"; then + fail references "$page" "requires a capability the capabilities page does not define: $value" + fi + ;; code) s_references=$((s_references+1)) if [ "$(printf '%s' "$defined" | tr -d ' ')" = "" ]; then diff --git a/.claude/tools/knowledge-check.test.sh b/.claude/tools/knowledge-check.test.sh index 0739dc4..ab4bc7d 100644 --- a/.claude/tools/knowledge-check.test.sh +++ b/.claude/tools/knowledge-check.test.sh @@ -273,6 +273,19 @@ printf 'Evidence: `repo:repos/one/missing.txt`.\n' >> "$wpage" got="$(bash "$KC" "$W" >"$T/out" 2>&1; echo $?)" check "a repo: path missing from the workspace fails" 1 "referenced repository path does not exist: repos/one/missing.txt" +echo "capabilities" +mkcap() { + mk "$R" + hdr "$KB/00-orientation/capabilities.md" "What each capability token grants and how to prove it" + printf '\n| Token | Blast | Grants | Proof (read-only) | Expected | Obtain |\n|---|---|---|---|---|---|\n| `TOOL-GIT` | 馃煝 | clone and inspect | `git --version` | ^git version | install Git |\n' >> "$KB/00-orientation/capabilities.md" +} +mkcap; awk '{print} /^title:/{print "requires: `TOOL-GIT`"}' "$KB/_readings/study.md" > "$T/x" && mv "$T/x" "$KB/_readings/study.md" +run "$R"; check "a page requiring a defined token passes" 0 "references resolve" +mkcap; awk '{print} /^title:/{print "requires: `TOOL-GIT`, `TOOL-NOPE`"}' "$KB/_readings/study.md" > "$T/x" && mv "$T/x" "$KB/_readings/study.md" +run "$R"; check "a page requiring an undefined token fails, naming it" 1 "does not define: TOOL-NOPE" +mk "$R"; awk '{print} /^title:/{print "requires: `TOOL-GIT`"}' "$KB/_readings/study.md" > "$T/x" && mv "$T/x" "$KB/_readings/study.md" +run "$R"; check "requiring a token with no capabilities page fails" 1 "no capabilities page" + echo "the base itself" mk "$R"; rm -rf "$R/docs"; run "$R"; check "no knowledge base at all" 1 "no knowledge base" mk "$R"; rm -f "$KB"/*.md "$KB"/*/*.md; run "$R"; check "a base with no page at all" 1 "no Markdown page found" diff --git a/.claude/tools/preflight.sh b/.claude/tools/preflight.sh new file mode 100644 index 0000000..e183bc2 --- /dev/null +++ b/.claude/tools/preflight.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Prove, read-only, which capabilities this machine and this identity hold, before a runbook is followed. +# +# bash .claude/tools/preflight.sh prove every token on the capabilities page +# bash .claude/tools/preflight.sh -c A,B prove only these tokens (what one page requires) +# bash .claude/tools/preflight.sh --list list the tokens and their proof commands, run nothing +# bash .claude/tools/preflight.sh --root act on another tree (fixtures, tests) +# +# The vocabulary is the project's, on one page: docs/knowledge-base/00-orientation/capabilities.md in a +# single repository, knowledge-base/00-orientation/capabilities.md above several. One table row per token: +# +# | `TOKEN` | blast | grants | `proof command` | expected | obtain | +# +# TOKEN a stable identifier: uppercase letters, digits and hyphens. A page names it in its `requires:` +# header field, and the knowledge check refuses a token this page does not define. +# blast one marker: 馃煝 read-only or non-production 路 馃煛 changes non-production, or reads production +# data 路 馃敶 touches production. A task whose page requires a 馃敶 token is Tier 3 by that alone. +# proof ONE read-only command, in backticks; a pipe inside it is written \| as Markdown requires. +# expected an extended regex the command's output must match; empty means "exits 0"; the word +# UNRESOLVED means the project does not yet know how this capability is obtained, so the +# row names who to ask and the tool reports it without failing. +# obtain who grants it, or which page has the script. +# +# WHY: runbooks used to state their author's access ("we are cluster admin", "the clones live at +# C:/..."). That is a snapshot, not a precondition: a second reader cannot tell whether THEY can follow +# the page, and it fails silently when the person changes. The sharpest case, measured 2026-08-01 on +# one installation: a required-reviewer gate records a non-member's approve vote and leaves the pull +# request blocked, so tooling that votes reports success while nothing moves. A capability with a +# read-only proof is the answer; a note is not. +# +# The proof commands are code the repository owns, reviewed like any other file in it. Never run this +# against a page from a branch you have not read. +# +# Exit 0 = every requested token PASSED (UNRESOLVED does not fail). Exit 1 = a requested token FAILED or +# is not defined. Exit 2 = no capabilities page, no token rows, or a bad invocation. + +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" + +tokens=""; mode=""; root="" +while [ $# -gt 0 ]; do + case "$1" in + -c|--capabilities) tokens="${2:-}"; shift 2 ;; + --list) mode=list; shift ;; + --root) root="${2:-}"; shift 2 ;; + -h|--help) sed -n '2,33p' "$0"; exit 0 ;; + *) echo "unknown option: $1"; exit 2 ;; + esac +done +if [ -n "$root" ]; then + [ -d "$root" ] || { echo "root does not exist: $root"; exit 2; } + ROOT="$(cd "$root" && pwd)" +fi +cd "$ROOT" || { echo "cannot cd to $ROOT"; exit 2; } + +page="" +for p in docs/knowledge-base/00-orientation/capabilities.md knowledge-base/00-orientation/capabilities.md; do + [ -f "$p" ] && { page="$p"; break; } +done +[ -n "$page" ] || { echo "no capabilities page: expected docs/knowledge-base/00-orientation/capabilities.md (or knowledge-base/... above several clones)"; exit 2; } + +TMP="$(mktemp -d 2>/dev/null || mktemp -d -t preflight)" +trap 'rm -rf "$TMP"' EXIT + +# One row per token. Cells are split on unescaped pipes; a \| inside a cell is a literal pipe. +awk ' +/^\| *`[A-Z][A-Z0-9-]+` *\|/ { + line = $0; sub(/\r$/, "", line) + gsub(/\\\|/, "\001", line) + n = split(line, c, "|") + for (i = 1; i <= n; i++) { sub(/^[ \t]+/, "", c[i]); sub(/[ \t]+$/, "", c[i]); gsub(/\001/, "|", c[i]) } + tok = c[2]; gsub(/`/, "", tok) + proof = c[5]; sub(/^`/, "", proof); sub(/`$/, "", proof) + printf "%s\037%s\037%s\037%s\037%s\n", tok, c[3], proof, c[6], c[7] +}' "$page" > "$TMP/rows" +[ -s "$TMP/rows" ] || { echo "the capabilities page has no token rows; expected: | \`TOKEN\` | blast | grants | \`proof\` | expected | obtain |"; exit 2; } + +if [ "$mode" = list ]; then + echo "capabilities from $page" + while IFS=$'\037' read -r tok blast proof expected obtain; do + printf ' %-24s %s %s\n' "$tok" "$blast" "${proof:-(no proof)}" + done < "$TMP/rows" + exit 0 +fi + +if [ -z "$tokens" ]; then requested="$(cut -d $'\037' -f1 "$TMP/rows" | tr '\n' ' ')" +else requested="$(printf '%s' "$tokens" | tr ',' ' ')"; fi + +pass=0; fail=0; unres=0 +echo "preflight page=$page" +for t in $requested; do + row="$(awk -F'\037' -v t="$t" '$1 == t { print; exit }' "$TMP/rows")" + if [ -z "$row" ]; then printf ' FAIL %-22s not defined on %s\n' "$t" "$page"; fail=$((fail+1)); continue; fi + IFS=$'\037' read -r tok blast proof expected obtain <<< "$row" + if [ "$expected" = "UNRESOLVED" ]; then + printf ' ???? %-22s UNRESOLVED: nobody has written how this is obtained. Ask: %s\n' "$t" "${obtain:-(no one named)}" + unres=$((unres+1)); continue + fi + if [ -z "$proof" ]; then printf ' FAIL %-22s no proof command on the page\n' "$t"; fail=$((fail+1)); continue; fi + if command -v timeout >/dev/null 2>&1; then out="$(timeout 30 bash -c "$proof" 2>&1)"; rc=$? + else out="$(bash -c "$proof" 2>&1)"; rc=$?; fi + first="$(printf '%s' "$out" | head -1 | cut -c1-70)" + if [ "$rc" -eq 0 ] && { [ -z "$expected" ] || printf '%s\n' "$out" | grep -qE -e "$expected"; }; then + printf ' PASS %-22s %s %s\n' "$t" "$blast" "$first"; pass=$((pass+1)) + else + if [ "$rc" -ne 0 ]; then why="exit $rc"; else why="output did not match /$expected/"; fi + printf ' FAIL %-22s %s %s. %s. Obtain: %s\n' "$t" "$blast" "$why" "${first:-(no output)}" "${obtain:-(not written down)}" + fail=$((fail+1)) + fi +done +echo +echo "$pass passed, $fail failed, $unres unresolved." +[ "$fail" -eq 0 ] || exit 1 +exit 0 diff --git a/.claude/tools/status.sh b/.claude/tools/status.sh new file mode 100644 index 0000000..cad43b8 --- /dev/null +++ b/.claude/tools/status.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# What the estate is doing right now, computed rather than remembered. +# +# bash .claude/tools/status.sh run the project's probes and the declared deadlines +# bash .claude/tools/status.sh --check parse the two inputs, run no probe (verify.sh uses this) +# bash .claude/tools/status.sh --canary MUST FAIL: proves a RED is reported and a malformed line is refused +# bash .claude/tools/status.sh --root act on another tree (fixtures, tests) +# +# Two inputs, both optional, both owned by the project, never by this kit: +# +# .claude/tools/status.project.sh the project's own read-only probes. One finding per line on stdout: +# LEVEL section subject detail remedy +# LEVEL is RED, AMBER, OK or SKIP. RED and AMBER need a remedy, and the +# remedy is something a person can paste, not a worry. +# .claude/deadlines.conf dates no system can answer (a vendor's billing change, a renewal set +# by hand). One per line: +# :: :: +# AMBER from warn_days out, RED from a quarter of that (at most 21 days), +# RED once it is past. Lead-time-relative, so a 120-day runway does not +# shout four months early and a 14-day item does not stay green until +# the day before. +# +# THE RULE THIS ENCODES: if it can be read from a system, it is not a document. On one installation a +# spot-check on 2026-08-24 found five of seven written operational claims stale or wrong (a licence +# date, a certificate expiry, a deployed version, a restart count, a parking timer), every one of them +# computable from a live system. Prose is true for a week and then lies silently, and a reader cannot +# tell the two apart. A program has no memory and cannot be out of date. The only thing this trusts +# from a file is deadlines.conf, which must hold only dates that genuinely cannot be read anywhere. +# +# Two design choices, both paid for. Symptoms are reported as a condition, not a list: a probe that +# prints twenty tombstones for one full disk trains the reader to ignore it, so status.project.sh +# should count and name the cause. And a finding that cannot be parsed is a RED of its own, because a +# check whose output nobody can read cannot gate. +# +# Exit 0 = nothing RED. Exit 1 = at least one RED, so it works as a gate. Exit 2 = a bad invocation. +# It never writes, approves, cancels or deploys anything; a probe in status.project.sh that does is a +# defect in that script, and the rules say so. + +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" + +mode=""; root="" +while [ $# -gt 0 ]; do + case "$1" in + --canary|--check) mode="$1"; shift ;; + --root) root="${2:-}"; shift 2 ;; + -h|--help) sed -n '2,36p' "$0"; exit 0 ;; + *) echo "unknown option: $1"; exit 2 ;; + esac +done +if [ -n "$root" ]; then + [ -d "$root" ] || { echo "root does not exist: $root"; exit 2; } + ROOT="$(cd "$root" && pwd)" +fi +cd "$ROOT" || { echo "cannot cd to $ROOT"; exit 2; } + +PROJECT=".claude/tools/status.project.sh" +DEADLINES=".claude/deadlines.conf" +TMP="$(mktemp -d 2>/dev/null || mktemp -d -t status)" +trap 'rm -rf "$TMP"' EXIT +: > "$TMP/RED"; : > "$TMP/AMBER"; : > "$TMP/OK"; : > "$TMP/SKIP" + +# Records are joined with the unit separator (037), never a tab: `read` treats a tab as whitespace and +# collapses two in a row, so an empty subject would shift the detail into its place. +emit() { # LEVEL section subject detail remedy -> 1 when LEVEL is not one of the four + case "$1" in RED|AMBER|OK|SKIP) ;; *) return 1 ;; esac + printf '%s\037%s\037%s\037%s\n' "$2" "$3" "$4" "$5" >> "$TMP/$1" +} + +# Seconds since the epoch for a yyyy-mm-dd: GNU date first, BSD date second. +epoch_of() { + local e + e="$(date -d "$1" +%s 2>/dev/null)" && [ -n "$e" ] && { printf '%s' "$e"; return 0; } + e="$(date -j -f '%Y-%m-%d' "$1" +%s 2>/dev/null)" && [ -n "$e" ] && { printf '%s' "$e"; return 0; } + return 1 +} +days_until() { + local e + e="$(epoch_of "$1")" || return 1 + echo $(( (e - $(date +%s)) / 86400 )) +} +trim() { printf '%s' "$1" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//'; } + +# --- the declared deadlines -------------------------------------------------------------------- +read_deadlines() { # $1 = "check" to validate only + local checkonly="${1:-}" valid=0 + local n=0 line head rest what where d warn owner days lvl red detail + [ -f "$DEADLINES" ] || { emit SKIP deadlines "$DEADLINES" "no declared deadlines file" ""; return 0; } + while IFS= read -r line || [ -n "$line" ]; do + line="${line%$'\r'}" + case "$(trim "$line")" in ''|'#'*) continue ;; esac + n=$((n+1)) + head="${line%%::*}"; rest="${line#*::}" + if [ "$rest" = "$line" ]; then + emit RED deadlines "line $n" "no '::' separator: $(printf '%s' "$line" | head -c 100)" "write: :: :: " + continue + fi + what="$(trim "${rest%%::*}")"; where="${rest#*::}" + [ "$where" = "$rest" ] && where="" + where="$(trim "$where")" + # shellcheck disable=SC2086 + set -- $head + d="${1:-}"; warn="${2:-}"; owner="${3:-UNASSIGNED}" + case "$d" in + [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; + *) emit RED deadlines "line $n" "date is not yyyy-mm-dd: '$d'" "fix $DEADLINES"; continue ;; + esac + case "$warn" in ''|*[!0-9]*) emit RED deadlines "line $n" "warn_days is not a number: '$warn'" "fix $DEADLINES"; continue ;; esac + [ -n "$what" ] || { emit RED deadlines "line $n" "no 'what' between the separators" "fix $DEADLINES"; continue; } + valid=$((valid+1)) + [ "$checkonly" = "check" ] && continue + days="$(days_until "$d")" || { emit RED deadlines "line $n" "date does not parse on this machine: $d" "fix $DEADLINES, or install GNU date"; continue; } + red=$(( warn / 4 )); [ "$red" -gt 21 ] && red=21; [ "$red" -lt 1 ] && red=1 + if [ "$days" -lt 0 ]; then lvl=RED; detail="overdue by $(( -days )) d ($d): $what" + elif [ "$days" -le "$red" ]; then lvl=RED; detail="in $days d ($d): $what" + elif [ "$days" -le "$warn" ]; then lvl=AMBER; detail="in $days d ($d): $what" + else lvl=OK; detail="in $days d ($d): $what"; fi + emit "$lvl" deadlines "$owner" "$detail" "$where" + done < "$DEADLINES" + [ "$n" -eq 0 ] && emit SKIP deadlines "$DEADLINES" "the file has no deadline lines" "" + [ "$checkonly" = "check" ] && [ "$valid" -gt 0 ] && emit OK deadlines "$DEADLINES" "$valid line(s) parse (not evaluated)" "" + return 0 +} + +# --- the project's own probes ------------------------------------------------------------------ +parse_findings() { # file source-name + local n=0 lvl sec subj det rem line + while IFS= read -r line || [ -n "$line" ]; do + line="${line%$'\r'}" + [ -n "$line" ] || continue + n=$((n+1)) + lvl=""; sec=""; subj=""; det=""; rem="" + IFS=$'\037' read -r lvl sec subj det rem <<< "$(printf '%s' "$line" | tr '\t' '\037')" + if [ -z "$det" ] || ! emit "$lvl" "$sec" "$subj" "$det" "$rem"; then + emit RED status "$2 line $n" "malformed finding, not LEVELsectionsubjectdetailremedy: $(printf '%s' "$line" | head -c 120)" "fix the probe; a finding that cannot be parsed cannot gate" + continue + fi + case "$lvl" in RED|AMBER) [ -n "$rem" ] || emit RED status "$2 line $n" "$lvl finding without a remedy: $subj" "give every RED and AMBER a remedy a person can paste; a finding without a next step is a worry, not a check" ;; esac + done < "$1" + [ "$n" -eq 0 ] && emit SKIP estate "$2" "printed no findings" "a probe that finds nothing should print OK lines, so silence stays distinguishable from a probe that did not run" + return 0 +} +run_project() { + local rc + [ -f "$PROJECT" ] || { emit SKIP estate "$PROJECT" "no status.project.sh; the estate answers nothing yet" "write it: one finding per line, LEVELsectionsubjectdetailremedy, read-only"; return 0; } + if command -v timeout >/dev/null 2>&1; then timeout 180 bash "$PROJECT" > "$TMP/out" 2> "$TMP/err"; rc=$? + else bash "$PROJECT" > "$TMP/out" 2> "$TMP/err"; rc=$?; fi + parse_findings "$TMP/out" "$PROJECT" + if [ "$rc" -ne 0 ]; then + emit RED estate "$PROJECT" "exited $rc$( [ -s "$TMP/err" ] && printf ': %s' "$(head -c 200 "$TMP/err" | tr '\n' ' ')" )" "run it by hand: bash $PROJECT" + fi + return 0 +} + +# --- report -------------------------------------------------------------------------------------- +count() { wc -l < "$TMP/$1" | tr -d ' '; } +render() { + local lvl sec subj det rem + echo "status.sh root=$ROOT $(date -u +%Y-%m-%dT%H:%MZ)" + for lvl in RED AMBER OK SKIP; do + [ -s "$TMP/$lvl" ] || continue + echo; echo "-- $lvl ($(count "$lvl")) --" + while IFS=$'\037' read -r sec subj det rem; do + printf ' %-10s %s\n' "$sec" "$subj" + printf ' %s\n' "$det" + [ -n "$rem" ] && printf ' > %s\n' "$rem" + done < "$TMP/$lvl" + done + echo + echo "$(count RED) RED, $(count AMBER) AMBER, $(count OK) OK, $(count SKIP) not checked." + [ "$(count RED)" -gt 0 ] && echo "Lead with the RED items." + return 0 +} + +case "$mode" in + --canary) + printf 'RED\tcanary\tsynthetic\tthis RED is injected by --canary\tnothing to do, it is the canary\n' > "$TMP/canary" + printf 'this line is malformed on purpose and must be reported\n' >> "$TMP/canary" + parse_findings "$TMP/canary" "canary" + render + if [ "$(count RED)" -lt 2 ]; then echo "canary: the parser LOST a RED; status.sh cannot gate and its greens are void" + else echo "canary: this run must fail, and it does: a synthetic RED and a malformed line were both reported"; fi + exit 1 ;; + --check) + read_deadlines check + if [ -f "$PROJECT" ]; then + if bash -n "$PROJECT" 2> "$TMP/syn"; then emit OK estate "$PROJECT" "parses (not run)" "" + else emit RED estate "$PROJECT" "does not parse: $(head -c 200 "$TMP/syn" | tr '\n' ' ')" "fix the script"; fi + fi + render + [ "$(count RED)" -eq 0 ] || exit 1 + exit 0 ;; + "") + read_deadlines + run_project + render + [ "$(count RED)" -eq 0 ] || exit 1 + exit 0 ;; +esac diff --git a/.claude/tools/verify.sh b/.claude/tools/verify.sh index 85c6d22..025468e 100644 --- a/.claude/tools/verify.sh +++ b/.claude/tools/verify.sh @@ -62,6 +62,12 @@ if [ "$mode" = "--canary" ]; then fi rm -rf "$c" fi + # Third, that the estate gate can report a RED: status.sh is fed a synthetic RED and a malformed line. + if [ -f .claude/tools/status.sh ]; then + if bash .claude/tools/status.sh --canary >/dev/null 2>&1; then + bad "status.sh goes red on a synthetic RED and a malformed line" "its canary passed; status.sh cannot gate, and the estate step of /orient is decoration" + else ok "status.sh goes red on a synthetic RED and a malformed line"; fi + fi bad "canary: this check must fail" "if this run reports success, the runner is lying and its greens are void" echo; echo " $PASS passed, $FAIL failed (canary run)"; exit 1 fi @@ -262,6 +268,30 @@ if [ -f .claude/hooks/guard-commands.sh ]; then else ok "guard-commands.sh blocks its self-test marker"; fi fi +# --- 12b 路 the estate: status.sh can go red, and the project's two inputs parse ------------------- +# The probes themselves are not run here: they reach live systems and belong to /orient. What a +# deterministic run can prove is that the gate reports a RED when given one, that deadlines.conf is +# well formed, and that status.project.sh at least parses. +if [ -f .claude/tools/status.sh ]; then + if bash .claude/tools/status.sh --canary >/dev/null 2>&1; then bad "status.sh goes red on a synthetic RED and a malformed line" "its canary passed; status.sh cannot gate" + else ok "status.sh goes red on a synthetic RED and a malformed line"; fi + if [ -f .claude/tools/status.project.sh ] || [ -f .claude/deadlines.conf ]; then + if stout="$(bash .claude/tools/status.sh --check 2>&1)"; then ok "status inputs parse ($(printf '%s\n' "$stout" | grep -E '^[0-9]+ RED' | head -1))" + else bad "status inputs parse" "$(printf '%s\n' "$stout" | grep -E '^ [a-z]|^ {13}' | head -3 | tr '\n' ' ')"; fi + else note "no status.project.sh and no deadlines.conf; the estate answers nothing yet"; fi +else + bad "status.sh present" "missing; /orient cannot compute what the estate is doing" +fi + +# --- 12c 路 the capabilities page parses, when there is one --------------------------------------- +cap=""; for p in docs/knowledge-base/00-orientation/capabilities.md knowledge-base/00-orientation/capabilities.md; do [ -f "$p" ] && cap="$p"; done +if [ -n "$cap" ]; then + if [ -f .claude/tools/preflight.sh ]; then + if pfout="$(bash .claude/tools/preflight.sh --list 2>&1)"; then ok "capabilities page parses ($(printf '%s\n' "$pfout" | grep -c '^ ') tokens)" + else bad "capabilities page parses" "$(printf '%s\n' "$pfout" | tail -2 | tr '\n' ' ')"; fi + else bad "preflight.sh present" "there is a capabilities page at $cap and no tool to prove it"; fi +fi + # --- 13 路 layout resolves when a workspace marker exists ------------------------------------------ if [ -f .workspace ]; then . .claude/tools/layout.sh @@ -281,6 +311,12 @@ if [ "$mode" = "--hooks" ]; then else bad "knowledge-check.test.sh passed" "an invalid record passed, or a valid one failed; see above"; fi else bad "knowledge-check.test.sh present" "missing"; fi + echo; note "estate cases (status.sh and preflight.sh run against fixture trees, one per invariant)" + if [ -f .claude/tools/estate.test.sh ]; then + if bash .claude/tools/estate.test.sh; then ok "estate.test.sh passed" + else bad "estate.test.sh passed" "a gate that should go red stayed green, or a good input was refused; see above"; fi + else bad "estate.test.sh present" "missing"; fi + echo; note "installer cases (the real installer run against a fake kit, one per update state)" if [ -f .claude/tools/install.test.sh ]; then if bash .claude/tools/install.test.sh; then ok "install.test.sh passed" diff --git a/README.md b/README.md index 85cb90e..8414f54 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ another, and nothing is copied between them. ## What is in the box -Four rules, ten skills, four hooks, nine tools. Install is a copy, and an update replaces only what you have not changed. +Four rules, ten skills, four hooks, twelve tools. Install is a copy, and an update replaces only what you have not changed. | Part | What it does | |---|---| @@ -57,7 +57,9 @@ Four rules, ten skills, four hooks, nine tools. Install is a copy, and an update | **four hooks** | a secret value is blocked before it is written; a test weakened, skipped, or deleted blocks the turn, measured from the baseline of the task this session carries when there is one, and from `HEAD` otherwise, and it keeps blocking until the test is repaired or you authorize that exact change; a destructive-command list that starts empty and grows from incidents; that same task's brief re-read after a compaction | | **`baseline.sh`** | seals the task's starting point into the agreed brief at the owner's yes: the approval time, the tier, the digest of the agreed text, the commit of every checkout, and a second digest over all of those, so the starting point cannot be moved by a hand edit. It lists the files that were already dirty, and binds the brief to this Claude session so both hooks know which agreement is in flight rather than taking the newest file; unbound, both fall back and say so. `check` at hand-back reads it back and refuses a top-tier task with neither a completed independent review nor an owner's waiver, or one whose code has moved past the commit that was reviewed. `allow-test-change` is the one escape from the Stop hook: it records a file, the exact content it ends at, and why, so that change is allowed and the next one is not | | **`knowledge-check.sh`** | the knowledge base's mechanical floor: every durable page carries the header the rules require, the values with an objective shape have it, and every reference it can identify without guessing resolves, which is local links, page-relative references, `repo:` paths, `commit:` shas, and decision codes. It also proves a short curated list of counts the README states still matches the tree. It decides structure, never truth | -| **`verify.sh`** | the checks that exist here, a canary that must fail, and three test suites that prove the hooks, the knowledge checks, and the updater can each go red | +| **`status.sh`** | what the estate is doing right now, computed rather than remembered: it runs the project's own read-only probes (`status.project.sh`, one finding per line with a level, a subject, a detail and a remedy a person can paste) and the declared deadlines (`deadlines.conf`, only the dates no system can answer, with a lead time and an owner), exits 1 on anything RED, and `/orient` runs it first and leads with the RED lines. A finding it cannot parse is a RED of its own, and its canary must fail | +| **`preflight.sh`** | proves, read-only, which capabilities this machine and identity hold before a runbook is followed: the tokens a page names in its `requires:` header, each defined on the capabilities page with a blast marker, a proof command, the expected output and how to obtain it. An UNRESOLVED row is reported, never invented around, and the knowledge check refuses a token the page does not define | +| **`verify.sh`** | the checks that exist here, a canary that must fail, and four test suites that prove the hooks, the knowledge checks, the updater, and the estate gate can each go red | Every hook ships in PowerShell and in Bash, because the host launches whichever the machine runs. The tools are Bash, so Windows needs Git Bash; that is stated again under Install, because it is the @@ -131,6 +133,9 @@ disposable folder. 9. **Keep negative results.** A thing that turned out not to be true saves the next person a day. 10. **The grader must not be the worker.** A fresh-context review is the default; a second model is optional and read-only. +11. **If it can be read from a system, it is not a document.** A version, an expiry, a count written + into a page is true for a week and then lies silently; write the probe instead, and keep in a + file only the dates no system can answer. ## The knowledge base @@ -234,8 +239,8 @@ way. rules/ standing-orders 路 writing 路 working-here 路 knowledge-base 路 methodology (this repo's own) skills/ orient 路 work 路 codex-relay 路 test-guide 路 pr 路 board 路 record 路 handoff 路 explain 路 summarize hooks/ guard-secrets 路 guard-commands 路 verify-on-finish 路 resume-brief, each .ps1 and .sh - tools/ layout.sh 路 verify.sh 路 baseline.sh 路 knowledge-check.sh 路 install.sh 路 install.ps1 - hooks.test.sh 路 knowledge-check.test.sh 路 install.test.sh 路 stop-hook-canary.test.js + tools/ layout.sh 路 verify.sh 路 baseline.sh 路 knowledge-check.sh 路 status.sh 路 preflight.sh 路 install.sh 路 install.ps1 + hooks.test.sh 路 knowledge-check.test.sh 路 install.test.sh 路 estate.test.sh 路 stop-hook-canary.test.js install-manifest.txt what the installer gave you, so an update can tell your edits from ours knowledge-drift.conf this repository's curated list of counts the README states settings.json the hook wiring diff --git a/START-HERE.md b/START-HERE.md index 3d6d445..26a5d3c 100644 --- a/START-HERE.md +++ b/START-HERE.md @@ -113,7 +113,12 @@ would otherwise get wrong, dated and re-checkable by command: - **Running it:** the build command, the start command, the test command, and how long each takes. - **Verify:** what `.claude/tools/verify.project.sh` runs; write that script from the commands above, so `verify.sh --full` runs the project's own checks. -- **Deployed-state check:** the command or endpoint, and what it returns. +- **Deployed-state check:** the command or endpoint, and what it returns, written as a probe in + `.claude/tools/status.project.sh` (one finding per line: level, section, subject, detail, remedy), + with the dates no system can answer in `.claude/deadlines.conf`. `/orient` runs `status.sh` first. +- **Capabilities:** `00-orientation/capabilities.md` in the knowledge base, one row per token with a + blast marker, a read-only proof command, the expected output and how to obtain it; the runbook + pages name them in `requires:`, and `preflight.sh -c` proves them. - **Tracker:** kind, organisation and project, the token variable name (never the value), and the fields, states, and custom fields measured from real items. - **Data posture** and any project additions to the confirm-before list. @@ -137,6 +142,7 @@ speculative. Everything written here follows `.claude/rules/knowledge-base.md`. bash .claude/tools/verify.sh # all green bash .claude/tools/verify.sh --hooks # every hook fired by hand, both shells bash .claude/tools/verify.sh --canary # must FAIL; a runner that passes it is lying +bash .claude/tools/status.sh --canary # must FAIL too; the estate gate has its own runner ``` Copy `.github/workflows/verify.yml` from the kit into the project's workflows if it uses GitHub @@ -165,6 +171,8 @@ Run these and report the results honestly. Do not report a pass you did not obse | 15 | after a compaction, continue a task with a brief in `working//brief.md` | the brief is back in context without anyone re-planning | | 16 | `/goal` with the verify command as the condition | the session keeps checking after each turn until it passes | | 17 | `/board` told to "just close it" | it proposes and stops; nothing is written that turn | +| 18 | `status.sh --canary` | fails, and says a synthetic RED and a malformed line were both reported | +| 19 | a page's `requires:` names a token the capabilities page does not define | `verify.sh` goes red naming the token | ## 8 路 How a session runs from here diff --git a/docs/knowledge-base/00-orientation/index.md b/docs/knowledge-base/00-orientation/index.md index 3fce074..72a1ccf 100644 --- a/docs/knowledge-base/00-orientation/index.md +++ b/docs/knowledge-base/00-orientation/index.md @@ -2,7 +2,7 @@ title: Where every page is, and what each one settles status: verified as_of: 2026-09-05 -last_verified: 2026-09-05 +last_verified: 2026-09-22 verification_method: Listing of this folder at the commit that introduced it scope: This knowledge base only confidence: High. This page is maintained by hand and checked against the folder listing @@ -44,6 +44,7 @@ reverify_when: Every time a page is added, superseded, or removed | `_investigations/2026-09-05-active-task/methodology.md` | how a Claude session says which task it is carrying, where the session id comes from, and the runs that prove a relay brief can no longer be restored as the agreement | | `_investigations/2026-09-05-installer-update/methodology.md` | how a copied kit is upgraded without destroying local changes, what the manifest records, why project-local copy stays canonical now that plugins exist, and the runs behind each update state | | `_investigations/2026-09-07-confirming-evidence/methodology.md` | why a Tier 3 change passed three design reviews, 240 tests and its own mutation test while failing open on the case it existed to prevent; the two method defects that caused it | +| `_investigations/2026-09-22-estate-checks/methodology.md` | what the estate checks add (`status.sh`, `preflight.sh`, the `requires:` field, the reasoning traps), the failures on the source installation that paid for them, and how each tool was shown to go red | | `_investigations/2026-09-05-knowledge-integrity/methodology.md` | which knowledge-base rules became executable checks, which stayed conventions and why, the measurement that put the line there, and the drift the checks found on arrival | | `_investigations/2026-09-05-tier3-review/methodology.md` | what the Tier 3 review contract requires, which part a check decides and which part only the people can, and the runs for reviewed, waived, and neither | | `99-pending.md` | everything found and not acted on | diff --git a/docs/knowledge-base/99-pending.md b/docs/knowledge-base/99-pending.md index 2bcfd4e..c62073e 100644 --- a/docs/knowledge-base/99-pending.md +++ b/docs/knowledge-base/99-pending.md @@ -68,3 +68,4 @@ This is the index of what is open, not the evidence for it. - `P2` The blueprint's `make-launch.sh`, which generates the browser-pane launch profile from the resolved layout, was not brought into the kit. Add it if a shape-B project needs `launch.json`. - `P2` Two worktree traps measured by GSD, for `working-here.md` when the kit first uses a worktree: `refs/stash` is shared across linked worktrees, and `git clean` in a fresh worktree deletes what a linked checkout still needs. Evidence: `_readings/harness-benchmark-takeaways-2026-09-05.md`, section 8. - `P2` `is_checkout_root()` compares `pwd` against `cd $(git rev-parse --show-toplevel) && pwd`, which disagrees with itself when a checkout sits under the MSYS `/tmp` mount and is reached by its `/c/...` name: `pwd` answers `/c/Users//AppData/Local/Temp/...` while the same directory reached through git answers `/tmp/...`, so `baseline.sh` refuses with "git does not report this folder as a checkout root". Measured 2026-09-07 while building the mid-operation refusal; the test suites avoid it because `mktemp -d` already returns a `/tmp/...` path. Harmless for a normal project checkout. It fails for any repository that lives under the Windows temp directory. +- `P2` `status.sh` and `preflight.sh` are bash only, like every tool here, so a Windows adopter without Git Bash cannot run the estate step of `/orient`; the hooks are unaffected. Evidence: `_investigations/2026-09-22-estate-checks/methodology.md`. diff --git a/docs/knowledge-base/_investigations/2026-09-22-estate-checks/methodology.md b/docs/knowledge-base/_investigations/2026-09-22-estate-checks/methodology.md new file mode 100644 index 0000000..153a04d --- /dev/null +++ b/docs/knowledge-base/_investigations/2026-09-22-estate-checks/methodology.md @@ -0,0 +1,87 @@ +--- +title: What the estate checks add (status.sh, preflight.sh, the requires field, the reasoning traps), the failures on the source installation that paid for them, and how each tool was shown to go red +status: verified +as_of: 2026-09-22 +last_verified: 2026-09-22 +verification_method: The fixture cases in estate.test.sh and the three new cases in knowledge-check.test.sh, run on the owner's Windows machine in Git Bash on 2026-09-22; verify.sh, verify.sh --hooks and verify.sh --canary run on the same tree. The failures cited from the source installation are the owner's own measurements, dated, with the project's identifying detail left out +scope: The change decided as D-22. Not the source installation's status program itself, which stays private and is nine sections and 739 lines of probes against one estate; this kit takes its contract and its rule +confidence: High for what the two tools do, which the suite exercises case by case. Medium for the rule about values in prose, which stays advice: no check can tell a measurement from a claim +known_gaps: No adopter has a status.project.sh with real probes yet; the only one that exists at this date is a stub in the owner's second installation. The proof commands on a capabilities page are run as written, so the page is trusted like any script in the repository, and that trust is stated rather than enforced. The reasoning traps are recorded from five incidents on one installation; nothing says they generalise +reverify_when: The first adopter writes real probes, or a proof command is found to mutate state, or a deadlines line needs a field the line format cannot carry +--- + +## The gap this closes + +The kit asked the right question and left the answer as prose. `/orient` step 4 said "what is +deployed, when the project rule names a way to check", and the project rule template held one line +for it. A knowledge base written under this kit could still say "the licence expires on the 15th" +or "the deployed image is 1.1.606" as a sentence, and nothing would ever compare the sentence with +the system. The source installation measured what that costs. + +## What the source installation measured + +| Date | What was found | What it taught | +|---|---|---| +| 2026-08-24 | A spot-check of seven written operational claims (a licence milestone, a certificate expiry, a deployed image, a restart count, a parking timer) found five stale or wrong. The knowledge base held about 1,600 hand-written image tags and 2,800 dated claims. Every one of the five was computable from a live system | if it can be read from a system, it is not a document. A program has no memory and cannot be out of date; the only dates worth keeping in a file are the ones no system can answer | +| 2026-08-01 | Runbooks stated their author's access ("cluster admin on all clusters", "the clones live at C:/..."). A required-reviewer gate recorded a non-member's approve vote and left the pull request blocked, so tooling that votes reported success while nothing moved | a runbook states its capabilities, not its author, and each capability carries a read-only proof; a note cannot fail | +| 2026-08-24 | A fix recorded as proven by an offline replay (12 of 77 cases to 44 of 77) had fired zero times in production: the replay supplied an input the running system never produces | a green test on an unreachable path is not evidence; after a deploy, look for the success line the fix emits | +| 2026-08-02 to 2026-08-08 | Three incidents in a week measured the size of a mechanism and reported it as the effect: a migration "revoking 353 entitlements" over a collection with zero documents; "86 of 88 messages dropped" where 52 were correct skips and the rest were the system's own echoes; a dead-token alarm where 1,083 sends had succeeded on the same channels | count what the customer has, not what the code touches; trace one item end to end before trusting an aggregate | +| 2026-09-20 | A service with 26 log lines a day was written down as idle; it carried all console traffic and logged at Warning | a quiet log is a mechanism; the callers are the effect | + +The first two rows are the reason for the two tools. The last three are the reason `working-here.md` +gains a section for traps that are about judgement rather than shells. + +## What was built + +- **`status.sh`** runs `.claude/tools/status.project.sh`, the project's own read-only probes, one + finding per line (`LEVEL`, section, subject, detail, remedy, tab-separated), and + `.claude/deadlines.conf`, one declared date per line with a lead time and an owner. It groups the + findings RED first, exits 1 on any RED, treats a line it cannot parse as a RED of its own, and + requires a remedy on every RED and AMBER. Its `--canary` injects a RED and a malformed line and + must fail. `/orient` runs it before anything else and leads the report with the RED lines. +- **`preflight.sh`** reads the capabilities page (one table row per token: blast marker, what it + grants, a read-only proof command, the expected output, how to obtain it) and runs the proofs for + the tokens asked for. `UNRESOLVED` in the expected column is reported and does not fail, so an + admitted gap stays visible instead of being invented around. +- **`knowledge-check.sh`** reads the `requires:` header field and refuses a token the capabilities + page does not define, and refuses tokens named when no capabilities page exists. +- **The rules:** `knowledge-base.md` gains the estate section (a value the estate can compute is a + check, a runbook states its capabilities, probes live beside the pages); `standing-orders.md` gains + the line that a 馃敶 token makes a task Tier 3 and the note that the two tools are checks, not hooks; + `working-here.md` gains the Reasoning section. +- **The skills:** `/orient` step 4 runs `status.sh`; `/work` proves a page's capabilities before the + one yes. + +## How each tool was shown to go red + +`estate.test.sh` builds a fixture tree per case and runs the real tool with `--root`. The cases, +each asserting the exit code and a message: an overdue date is RED; a date inside `warn_days` is +AMBER; a date inside a quarter of `warn_days` is RED, capped at 21 days; a bad date, a missing +separator and a file with no lines are each reported rather than skipped; an OK finding passes; a +RED finding fails with its remedy printed; a RED without a remedy is itself reported; a malformed +line and an unknown level are RED; a probe that exits non-zero is RED; a probe that prints nothing is +reported; `--check` catches a script that does not parse and runs no probe; `--canary` fails and +names both injections. For `preflight.sh`: a passing proof, a failing proof, an expected regex +matched and not matched, an escaped pipe inside a proof, an UNRESOLVED token, an undefined token, a +set with one failure, no page (exit 2), a page with no rows (exit 2). Thirty-one cases, thirty-one +passed on 2026-09-22. + +`knowledge-check.test.sh` gained three cases: a page requiring a defined token passes; a page +requiring an undefined token fails naming the token; a page requiring any token with no +capabilities page fails naming the missing page. + +`verify.sh` gained: the status canary must fail; `status.sh --check` must pass when either input +exists; the capabilities page must parse when it exists; the estate suite runs under `--hooks`. + +## Limits, stated once + +- Both tools are bash, like every tool in the kit, so a Windows adopter without Git Bash cannot run + them. The hooks are unaffected. +- The proof commands are executed as written. The page is trusted the way `verify.project.sh` is + trusted: it is code the repository owns and reviews. The tool says so and does nothing to enforce + it. +- `status.sh` cannot tell a probe that lies from a probe that is right. It gates on what the probe + prints. The rule that probes must be read-only is a rule. +- The rule "a value the estate can compute is a check, not a sentence" is advice. D-20's reasoning + holds: a prose check for version-shaped values would flag correct citations of other systems and + fail the pages that quote the rule. diff --git a/docs/knowledge-base/decisions.md b/docs/knowledge-base/decisions.md index 661293b..06509c8 100644 --- a/docs/knowledge-base/decisions.md +++ b/docs/knowledge-base/decisions.md @@ -1,8 +1,8 @@ --- title: Every decision in force, what each superseded, and when to revisit it status: verified -as_of: 2026-09-05 -last_verified: 2026-09-05 +as_of: 2026-09-22 +last_verified: 2026-09-22 verification_method: v2 entries transcribed from the owner's answers in the design session of 2026-09-04 and 2026-09-05. v1 entries migrated from DECISIONS.md at commit 43638ba, tag v1.5.1 scope: Decisions about the methodology and the kit. Not project-level decisions of any adopter confidence: High for what was decided and by whom. Medium for the "why" lines, which compress a long discussion @@ -16,6 +16,9 @@ by 路 reversible 路 revisit when 路 supersedes.** Codes: `D-` decided for v2, `V ## Decisions in force, v2 (2026-09-04 to 2026-09-05) +### D-22 路 The estate answers, the page does not: `status.sh`, `preflight.sh`, the `requires:` field, and the reasoning traps 路 2026-09-22 +**Decision:** two kit tools over three project-owned inputs. `status.project.sh` holds the project's read-only probes, one finding per line with a level, a section, a subject, a detail and a remedy a person can paste; `.claude/deadlines.conf` holds only the dates no system can answer, each with a lead time and an owner; `status.sh` runs both, groups RED first, exits 1 on any RED, treats a line it cannot parse as a RED of its own, and `/orient` runs it before anything else and leads its report with the RED lines. A runbook page names the capabilities it needs in a `requires:` header field, from `00-orientation/capabilities.md`, where each token carries a blast marker, what it grants, a read-only proof command, the expected output and how to obtain it, or UNRESOLVED and who to ask; `preflight.sh -c` proves them before `/work` asks for its one yes, and `knowledge-check.sh` refuses a token the page does not define. A token marked as touching production makes a task Tier 3 by itself. `working-here.md` gains a Reasoning section for the traps that are about judgement rather than shells. **Options:** keep the deployed-state check as one line in the project rule, which is what v2 had and which asked the question while leaving the answer as prose; ship a generic status program, rejected because every estate is different and only the contract travels; a JSON deadlines file, rejected because the kit avoids parser dependencies and a line carries a date, a lead time and an owner; a prose check that flags version-shaped values in pages, rejected for the reason D-20 rejected prose citation checks. **Why:** on one installation a spot-check on 2026-08-24 found five of seven written operational claims stale or wrong, every one computable from a live system; a required-reviewer gate that records a non-member's vote and stays blocked (2026-08-01) showed that "we have access" is one person's snapshot, not a precondition; and a fix recorded as proven by replay that fired zero times in production (2026-08-24) showed that the traps that cost the most are about reasoning. **By:** owner, 2026-09-22, from a comparison of that installation's knowledge base with this kit. **Reversible:** yes; two tools, one header field, three rule sections, and the skills' two steps. **Revisit when:** an adopter's probes outgrow a line format, a proof command is found to mutate state, or the Reasoning section grows past five entries without a merge. **Extends:** D-03's tool list, now twelve, and D-20, whose reference checks gain the `requires:` field. Evidence: `_investigations/2026-09-22-estate-checks/methodology.md`. + ### D-21 路 Project-local copy stays canonical, and the installer records what it installed so an update can be safe 路 2026-09-05 **Decision:** the canonical installation model remains project-local, copied, inspectable files that live with the adopter's repository, and `install.sh` now writes `.claude/install-manifest.txt` recording the release and the sha256 of every managed file as delivered. `install.sh --update` replaces only files whose current hash still equals the recorded one, preserves and lists everything else, adds new upstream files, never deletes, never merges, never writes conflict markers or backups, and never uses a timestamp. `--check` previews without writing. A locally modified managed file is a supported state and exits 0; exit 1 means the updater could not do its job. The managed inventory is taken from the distribution, never by scanning the target, so an adopter's own rules, skills, hooks, and tools are invisible to it. A path that exists with no recorded hash is left alone and recorded `unmanaged`, because a customization must never be recorded as an upstream basis. **Options:** make Claude plugins the canonical delivery, now that they exist, rejected because a global update would silently change engineering controls in a repository nobody was reviewing at the time, and because copied files are what make the controls readable in the project and pinned to its history; regenerate the manifest from the local tree, rejected because hashing the adopter's customization as though it were upstream defeats the mechanism at the next release; three-way merge or conflict markers, rejected as the opposite of deterministic and auditable; delete files removed upstream, rejected for v1 because deletion is more consequential than replacement. **Why:** the benchmark found install and update maturity to be the weakest part of a kit that is otherwise copied on purpose, and the copy decision is what creates the problem, so the same change pays for it. **By:** owner, in the distribution and updates brief of 2026-09-05. **Reversible:** yes; the manifest is one file and the classifier is one function in each installer. **Revisit when:** a real adopter upgrades a real installation, which has never happened, or when plugin delivery is evaluated as an optional transport. **Amends:** D-03, whose premise that the desktop app has no plugin surface was measured stale on 2026-09-05. The conclusion stands on different grounds: inspectable, reviewable, project-versioned, locally customizable controls. Plugin packaging is deferred as an optional future transport, not rejected as impossible; S-11 is unchanged as history. Evidence: `_investigations/2026-09-05-installer-update/methodology.md`.