← Files okrdevARCHIVED FILE
tests/check.sh
66.2 KB · Oct 2, 2026 · 00:30 UTC
#!/usr/bin/env bash
#
# okrdev checks itself. The doctrine behind this file is docs/testing.md.
#
# Two rules govern what may live here, both enforced by checks in this file:
#
# 1. Every assert traces to a real chafe. The `# chafe:` line above each
# check names it; check_chafe_comments fails the run if one is missing.
# A check nobody needed is speculative coverage, refused by construction.
#
# 2. Some divergence is deliberate. DO_NOT_FREEZE below lists what this
# suite must never assert away, and each entry is itself a negative test:
# if the cited divergence disappears, the run fails and somebody decides
# to graduate the rule through a doctrine PR or drop the entry. The list
# cannot rot into a stale comment, because it runs.
#
# No frameworks on purpose (docs/testing.md, "Ruled out"): bash, node, jq, diff.
# Runs in under a minute, needs no network and no secrets.
set -uo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1
failures=0
ok() { printf ' ok %s\n' "$1"; }
bad() { printf ' FAIL %s\n' "$1"; failures=$((failures + 1)); }
# The marked coach block, however it is embedded — a plain file, an installed
# CLAUDE.md, or a fenced quotation inside prose. Anchored so the markers named
# in running text (ai-coach.md explains them) are not mistaken for the block.
coach_block() { sed -n '/^<!-- okrdev:start -->$/,/^<!-- okrdev:end -->$/p' "$1"; }
# The canonical KR-line grammar, stripped of each copy's regex delimiters,
# flags and trailing punctuation so three different embeddings compare equal.
kr_grammar() { grep -h '\^KR:' "$1" | head -1 | sed 's/.*\(\^KR:.*\\s\*\$\).*/\1/'; }
# A check-in is HELD when its KR-confidence table carries at least one KR row.
# The existence of a week file proves nothing: rule 5 tells the coach to create
# one to log a judgment call mid-week, and /okrdev:checkin drafts one before the
# humans arrive. docs/rituals.md § the missed-cadence catch-up is the canonical
# definition of "held"; this is its one executable copy, shared by everything
# below that needs it, because a second copy is a second thing to drift.
#
# The header row is `| KR | DRI | ...`, so the digit is load-bearing: matching
# `^| KR` alone counts an empty table as populated, which is exactly how a
# never-held week silences a staleness rule.
held_checkin_rows() { grep -E '^\|[[:space:]]*KR[0-9]' "$1" 2> /dev/null; }
# Newest held check-in in a cycle's directory, or empty. Filenames are
# <yyyy>-W<nn>.md with a zero-padded ISO week, so lexical order is chronological
# order — including across a year boundary, which is why the year leads.
newest_held_checkin() {
local dir=$1 f newest=
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
held_checkin_rows "$f" > /dev/null || continue
if [ -z "$newest" ] || [[ "$f" > "$newest" ]]; then newest=$f; fi
done
printf '%s' "$newest"
}
# ── Checks ─────────────────────────────────────────────────────────────────
# chafe: the block drifted from its template for a month (PARKING_LOT 2026-08-04,
# "repo-root CLAUDE.md coach block one revision behind") — captured, never fixed.
check_coach_block() {
local template=templates/CLAUDE-okrdev.md
local copy status=0
for copy in CLAUDE.md docs/ai-coach.md; do
if diff -u <(coach_block "$template") <(coach_block "$copy") > /tmp/okrdev-coach-diff; then
ok "coach block: $copy matches $template"
else
bad "coach block: $copy has drifted from $template"
sed 's/^/ /' /tmp/okrdev-coach-diff
status=1
fi
done
rm -f /tmp/okrdev-coach-diff
return $status
}
# chafe: shipped to adopters unrun. Bad input exits 127 with "die: command not
# found" because die() is defined below its first call — found writing docs/testing.md.
check_branch_protection_bad_input() {
local out code
out=$(OKRDEV_REQUIRED_APPROVALS=abc bash templates/stack/branch-protection.sh owner/repo 2>&1)
code=$?
# Both halves matter: a nonzero-only assert passes on the broken script,
# which exits 127 instead of 1 and prints the wrong message.
if [ "$code" -eq 1 ] && printf '%s' "$out" | grep -q 'OKRDEV_REQUIRED_APPROVALS must be'; then
ok "branch-protection.sh: invalid approvals count exits 1 with its own message"
else
bad "branch-protection.sh: invalid approvals count exited $code saying: ${out:-(nothing)}"
fi
}
# chafe: none yet — the grammar's three copies agree today. Frozen before one
# drifts, because that is exactly how the coach block got away.
check_kr_grammar() {
local files=(docs/method.md skills/checkin/SKILL.md templates/github/workflows/okr-gate.yml)
local f distinct
for f in "${files[@]}"; do
if [ -z "$(kr_grammar "$f")" ]; then
bad "KR grammar: no pattern found in $f"
return 1
fi
done
distinct=$(for f in "${files[@]}"; do kr_grammar "$f"; done | sort -u | wc -l)
if [ "$distinct" -eq 1 ]; then
ok "KR grammar: all ${#files[@]} copies agree"
else
bad "KR grammar: ${#files[@]} copies, $distinct distinct patterns"
for f in "${files[@]}"; do printf ' %s: %s\n' "$f" "$(kr_grammar "$f")"; done
fi
}
# chafe: none yet — six files state this format and every override log line
# depends on it. Same reasoning as the KR grammar.
check_judgment_call_format() {
local expected=6 found distinct
found=$(grep -rc -- '— <who> — <reason>' docs/ skills/ templates/ 2>/dev/null | awk -F: '{n += $2} END {print n + 0}')
distinct=$(grep -rho -- '- <date> —[^`.]*' docs/ skills/ templates/ 2>/dev/null | sed 's/[[:space:]]*$//' | sort -u | wc -l)
if [ "$found" -eq "$expected" ] && [ "$distinct" -eq 1 ]; then
ok "judgment-call line: $expected copies, one format"
else
bad "judgment-call line: expected $expected copies of one format, found $found copies in $distinct shapes"
grep -rn -- '- <date> —' docs/ skills/ templates/ 2>/dev/null | sed 's/^/ /'
fi
}
# chafe: the manifests are what a `/plugin install` reads first, and nothing had
# ever checked they parse. Then the plugin-directory readiness audit (2026-08-08)
# found plugin.json short four fields a public listing renders — no `license`
# while shipping MIT, no `repository`, and no `homepage` after okrdev.com went
# live — and two descriptions free to drift apart, because only `.name` was ever
# compared across the pair. The Codex port then made it three manifests carrying
# the same identity, and added a failure that only surfaces in a portal preview
# after submission: a listing pointing at a logo that isn't there.
check_plugin_manifests() {
local plugin=.claude-plugin/plugin.json market=.claude-plugin/marketplace.json
local codex=.codex-plugin/plugin.json
local f version status=0 missing=() missing_assets=()
for f in "$plugin" "$market" "$codex"; do
if ! jq empty "$f" 2>/dev/null; then
bad "manifests: $f is not valid JSON"
return 1
fi
done
version=$(jq -r '.version' "$plugin")
if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
bad "manifests: plugin.json version '$version' is not semver"
return 1
fi
if [ "$(jq -r '.name' "$plugin")" != "$(jq -r '.plugins[0].name' "$market")" ]; then
bad "manifests: plugin.json and marketplace.json disagree on the plugin name"
status=1
fi
# The description is what a browsing user reads before installing anything.
# Two copies, no assert, is the same shape as the coach block's month of drift.
if [ "$(jq -r '.description' "$plugin")" != "$(jq -r '.plugins[0].description' "$market")" ]; then
bad "manifests: plugin.json and marketplace.json disagree on the description"
status=1
fi
for f in license homepage repository; do
[ -z "$(jq -r --arg k "$f" '.[$k] // ""' "$plugin")" ] && missing+=("$f")
done
# `keywords` must be an array: a bare string is a load error, not a warning,
# so the plugin would fail to load rather than list badly.
[ "$(jq -r 'if (.keywords | type) == "array" then (.keywords | length) else -1 end' "$plugin")" -lt 1 ] &&
missing+=("keywords[]")
if [ ${#missing[@]} -gt 0 ]; then
bad "manifests: plugin.json is missing directory-listing fields: ${missing[*]}"
status=1
fi
# Two directories, two manifests, one product. Shared identity is projected
# and compared as a whole rather than field by field: adding a field to the
# projection is one edit, and a field nobody thought to compare is exactly how
# the coach block drifted for a month.
local shared='{name, version, description, homepage, repository, license,
keywords, author: .author.name}'
if ! diff -u <(jq -S "$shared" "$plugin") <(jq -S "$shared" "$codex") > /tmp/okrdev-manifest-diff; then
bad "manifests: .claude-plugin and .codex-plugin disagree on shared identity"
sed 's/^/ /' /tmp/okrdev-manifest-diff
status=1
fi
rm -f /tmp/okrdev-manifest-diff
# A listing that points at a missing logo renders a broken card, and the only
# place that surfaces is the portal preview — after submission.
local asset
while read -r asset; do
[ -n "$asset" ] || continue
[ -f "${asset#./}" ] || missing_assets+=("$asset")
done < <(jq -r '.interface | (.logo // empty), (.composerIcon // empty)' "$codex")
if [ ${#missing_assets[@]} -gt 0 ]; then
bad "manifests: $codex points at assets that do not exist: ${missing_assets[*]}"
status=1
fi
[ $status -eq 0 ] &&
ok "manifests: 3 parse, version is semver, names/descriptions agree, shared identity matches, listing fields and assets present"
return $status
}
# chafe: KR2.2 — okrdev itself could not be installed without clicking through
# the /plugin dialog (parked 2026-07-23, promoted into the 2026-Q3 plan). The
# CLI's real gap: `claude plugin install` is a shell command, marketplace
# registration isn't. install.sh bridges it by writing the dialog's own state
# files, so this check pins that behavior against a stub claude in a throwaway
# CLAUDE_CONFIG_DIR — fixture clone from a local path, no network, per doctrine.
check_headless_install() {
local script=install.sh
if [ ! -x "$script" ]; then
bad "headless install: $script missing or not executable"
return 1
fi
local tmp status=0
tmp=$(mktemp -d) || { bad "headless install: mktemp failed"; return 1; }
# A fixture marketplace repo: the script's contract is "any git-cloneable
# source holding .claude-plugin/", not "GitHub specifically".
mkdir -p "$tmp/market"
cp -R .claude-plugin "$tmp/market/"
# GIT_DIR beats `git -C`, and a pre-push hook runs with GIT_DIR set — so the
# unqualified form built the fixture inside THIS repo and the check failed
# only when run the way the repo asks you to run it. The suite has to be
# hermetic in a hook, or the local rail is the one place it doesn't work.
local fixture_git=(env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE
-u GIT_OBJECT_DIRECTORY -u GIT_ALTERNATE_OBJECT_DIRECTORIES
-u GIT_COMMON_DIR git)
if ! { "${fixture_git[@]}" -C "$tmp/market" init -q -b main &&
"${fixture_git[@]}" -C "$tmp/market" add -A &&
"${fixture_git[@]}" -C "$tmp/market" -c user.email=check@okrdev -c user.name=check \
-c commit.gpgsign=false commit -qm fixture; }; then
bad "headless install: could not build the fixture marketplace repo"
rm -rf "$tmp"
return 1
fi
# A stub claude on PATH — the docs/testing.md pattern. It refuses the
# marketplace subcommand exactly like today's CLI, accepts plugin install,
# and records argv so the handoff to the documented command is assertable.
mkdir -p "$tmp/bin" "$tmp/claude-home"
cat > "$tmp/bin/claude" << 'STUB'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "${CLAUDE_STUB_LOG:?}"
case "${1-} ${2-}" in
"plugin marketplace") echo "error: unknown command 'marketplace'" >&2; exit 1 ;;
"plugin install") exit 0 ;;
*) exit 0 ;;
esac
STUB
chmod +x "$tmp/bin/claude"
# Failure path first, both halves (the branch-protection.sh lesson): a PATH
# with git but no claude must exit 1 with the script's own message, not 127
# with bash's. /usr/bin:/bin carries git on macOS and Linux; no installer
# puts claude there.
local out code
out=$(env PATH="/usr/bin:/bin" CLAUDE_CONFIG_DIR="$tmp/claude-home" bash "$script" 2>&1)
code=$?
if [ "$code" -eq 1 ] && printf '%s' "$out" | grep -q 'the claude CLI is not on PATH'; then
ok "headless install: missing claude exits 1 with its own message"
else
bad "headless install: missing claude exited $code saying: ${out:-(nothing)}"
status=1
fi
# Happy path: stub claude shadows any real one; everything else is real.
local log="$tmp/stub.log" home="$tmp/claude-home"
out=$(PATH="$tmp/bin:$PATH" CLAUDE_STUB_LOG="$log" CLAUDE_CONFIG_DIR="$home" \
bash "$script" --marketplace-source "$tmp/market" 2>&1)
code=$?
local knownf="$home/plugins/known_marketplaces.json"
if [ "$code" -eq 0 ] &&
[ -f "$home/plugins/marketplaces/okrdev/.claude-plugin/marketplace.json" ] &&
jq -e '.okrdev.source.source == "git" and .okrdev.installLocation != null' "$knownf" > /dev/null 2>&1 &&
[ "$(jq -r '.okrdev.source.url' "$knownf")" = "$tmp/market" ] &&
grep -q '^plugin install okrdev@okrdev$' "$log"; then
ok "headless install: registers the marketplace and hands off to claude plugin install"
else
bad "headless install: happy path failed (exit $code)"
printf '%s\n' "$out" | sed 's/^/ /'
status=1
fi
# Idempotency: a second run must not clobber, duplicate, or fail.
out=$(PATH="$tmp/bin:$PATH" CLAUDE_STUB_LOG="$log" CLAUDE_CONFIG_DIR="$home" \
bash "$script" --marketplace-source "$tmp/market" 2>&1)
code=$?
if [ "$code" -eq 0 ] && [ "$(jq -r 'keys | length' "$knownf")" = 1 ]; then
ok "headless install: second run leaves existing state alone and exits 0"
else
bad "headless install: second run exited $code (keys: $(jq -r 'keys | length' "$knownf" 2>/dev/null))"
printf '%s\n' "$out" | sed 's/^/ /'
status=1
fi
# The default, no-argument path — the one the README actually tells people to
# run, and the only one that produces the `source: github` entry shape and the
# origin re-point. Every assert above passes --marketplace-source, so without
# this the shape a real adopter receives is never executed: a typo in the
# github branch stays green all the way to their machine. Still no network —
# script_dir is this checkout, and `remote set-url` never contacts anything.
local home2="$tmp/claude-home-default" known2 origin
known2=$home2/plugins/known_marketplaces.json
out=$(PATH="$tmp/bin:$PATH" CLAUDE_STUB_LOG="$log" CLAUDE_CONFIG_DIR="$home2" \
bash "$script" 2>&1)
code=$?
origin=$("${fixture_git[@]}" -C "$home2/plugins/marketplaces/okrdev" remote get-url origin 2> /dev/null)
if [ "$code" -eq 0 ] &&
jq -e '.okrdev.source == {source: "github", repo: "backedbydata-co/okrdev"}' "$known2" > /dev/null 2>&1 &&
[ "$origin" = "https://github.com/backedbydata-co/okrdev.git" ]; then
ok "headless install: default path registers the github source and re-points origin upstream"
else
bad "headless install: default path failed (exit $code, origin: ${origin:-none})"
printf '%s\n' "$out" | sed 's/^/ /'
jq -c '.okrdev.source' "$known2" 2> /dev/null | sed 's/^/ source: /'
status=1
fi
# An existing-but-empty known_marketplaces.json — `touch` instead of the
# by-hand step's `{}`, or a writer that died mid-write. jq accepts a 0-byte
# file and its assignment filter then emits nothing, so the first version of
# this script reported a successful registration over a file it left empty,
# forever, on every re-run. Both JSON backends must refuse it instead.
local home3="$tmp/claude-home-empty" known3
known3=$home3/plugins/known_marketplaces.json
mkdir -p "$home3/plugins"
: > "$known3"
out=$(PATH="$tmp/bin:$PATH" CLAUDE_STUB_LOG="$log" CLAUDE_CONFIG_DIR="$home3" \
bash "$script" --marketplace-source "$tmp/market" 2>&1)
code=$?
if [ "$code" -eq 0 ] && jq -e '.okrdev.installLocation != null' "$known3" > /dev/null 2>&1; then
ok "headless install: an empty known_marketplaces.json is written, not silently skipped"
else
bad "headless install: empty known_marketplaces.json left unregistered (exit $code)"
printf '%s\n' "$out" | sed 's/^/ /'
status=1
fi
# Debris at the clone destination — a `git clone` killed by a CI timeout
# leaves a partial dir. Skipping the clone over it registers a marketplace
# pointing at nothing, and every re-run skips it again.
local home4="$tmp/claude-home-debris"
mkdir -p "$home4/plugins/marketplaces/okrdev"
out=$(PATH="$tmp/bin:$PATH" CLAUDE_STUB_LOG="$log" CLAUDE_CONFIG_DIR="$home4" \
bash "$script" --marketplace-source "$tmp/market" 2>&1)
code=$?
if [ "$code" -eq 1 ] && printf '%s' "$out" | grep -q 'is not a marketplace clone'; then
ok "headless install: debris at the clone destination exits 1 rather than registering it"
else
bad "headless install: debris at the clone destination exited $code saying: ${out:-(nothing)}"
status=1
fi
# KR2.2's 1.0 anchor is "following only the README" — a script the docs
# never point at is a script that does not exist for adopters.
local f docs_ok=0
for f in README.md docs/adoption.md; do
if ! grep -q 'install\.sh' "$f"; then
bad "headless install: $f never mentions install.sh"
docs_ok=1
status=1
fi
done
# Scoped to its own flag, not the shared one: an assert that reports only
# when every other assert passed is an assert that goes quiet exactly when
# the run is worth reading.
[ $docs_ok -eq 0 ] && ok "headless install: README.md and docs/adoption.md both point at install.sh"
rm -rf "$tmp"
return $status
}
# chafe: `source: "./"` in marketplace.json makes this repo the plugin
# directory, and the marketplace copies that whole directory into every
# installer's cache. dist/okrdev-repo.pdf — generated, tracked, unignored —
# was 6,157,688 bytes: 91% of everything an adopter downloaded, for a file
# nothing reads at runtime. Parked as issue #8 and mis-sized there as a repo
# restructure (M-L); it was one artifact (S). No other check measured the
# payload, so nothing would have noticed the next one.
check_payload() {
local limit=204800 scanned=0 oversize=() generated=() meta path size
while IFS=$'\t' read -r meta path; do
scanned=$((scanned + 1))
size=${meta##* }
case "$path" in dist/*) generated+=("$path") ;; esac
[ "$size" -gt "$limit" ] && oversize+=("$path (${size} bytes)")
done < <(git ls-tree -r --long HEAD)
# A scan that finds nothing passes vacuously — same trap check_chafe_comments
# guards against. This repo tracks ~70 files; insist the walk actually ran.
if [ "$scanned" -lt 20 ]; then
bad "payload: walked $scanned tracked files — the scan itself is broken"
return 1
fi
# Report both classes rather than returning on the first. A run that names
# only the artifact you already knew about invites a second red push for the
# one it stopped before reaching.
local status=0
if [ ${#generated[@]} -gt 0 ]; then
bad "payload: build output is tracked and ships to every installer: ${generated[*]}"
status=1
fi
if [ ${#oversize[@]} -gt 0 ]; then
bad "payload: tracked file over $((limit / 1024))KB — every adopter downloads it: ${oversize[*]}"
status=1
fi
[ $status -eq 0 ] && ok "payload: $scanned tracked files, none generated, largest under $((limit / 1024))KB"
return $status
}
# chafe: the OpenAI portal rejected `shortDescription` at 39 characters with a
# 30-character limit — discovered by a human typing into the form mid-submission,
# after the manifest had already passed every check here and been uploaded twice.
# A listing field that fails validation is found at the worst possible moment.
check_listing_copy() {
local manifest=.codex-plugin/plugin.json
local short problems=()
# Only the limit we have actually observed. The others are unknown, and
# inventing ceilings we have not seen fail is how a check starts lying.
short=$(jq -r '.interface.shortDescription // ""' "$manifest")
if [ -z "$short" ]; then
problems+=("shortDescription is empty")
elif [ "${#short}" -gt 30 ]; then
problems+=("shortDescription is ${#short} chars, over the portal's 30: \"$short\"")
fi
local field
for field in displayName longDescription developerName category; do
[ -n "$(jq -r --arg k "$field" '.interface[$k] // ""' "$manifest")" ] ||
problems+=("$field is empty")
done
# logo and composerIcon are *required*, and the portal insists each references a
# square image. check_plugin_manifests validates only assets that are referenced
# — its `// empty` emits nothing for an absent field, so a manifest that dropped
# one entirely sailed through. Presence is checked here; squareness with it,
# because "the file exists" and "the file is usable" are different claims.
local asset path w h vb
for field in logo composerIcon; do
asset=$(jq -r --arg k "$field" '.interface[$k] // ""' "$manifest")
if [ -z "$asset" ]; then
problems+=("$field is required and absent")
continue
fi
path=${asset#./}
[ -f "$path" ] || { problems+=("$field points at a missing file: $asset"); continue; }
case $path in
*.svg) ;;
*) problems+=("$field is $path — squareness is only verified for SVG, so this check needs extending before shipping it")
continue ;;
esac
# Root element only. A line-oriented scan reads the first width= it finds,
# which in assets/logo.svg is the inner <rect> — 512x512 by coincidence, and
# a wrong answer the moment that rect is resized. The root here carries no
# width/height at all ("the directory scales this"), so viewBox is the real
# source. Comments are stripped first: this file already broke once on XML
# comment parsing, and it says so in its own comment.
vb=$(node -e '
const fs = require("fs");
const src = fs.readFileSync(process.argv[1], "utf8").replace(/<!--[\s\S]*?-->/g, "");
const tag = src.match(/<svg\b[^>]*>/);
if (!tag) process.exit(0);
const num = v => { const n = parseFloat(v); return Number.isFinite(n) ? n : null; };
const mw = tag[0].match(/\swidth="([^"]+)"/), mh = tag[0].match(/\sheight="([^"]+)"/);
let w = mw ? num(mw[1]) : null, h = mh ? num(mh[1]) : null;
if (w === null || h === null) {
const box = tag[0].match(/\sviewBox="\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)/);
if (box) { w = num(box[1]); h = num(box[2]); }
}
if (w !== null && h !== null) console.log(w + " " + h);
' "$path")
w=${vb% *}; h=${vb#* }
if [ -z "$vb" ]; then
problems+=("$field: could not read dimensions from $path — cannot confirm it is square")
elif [ "$w" != "$h" ]; then
problems+=("$field is ${w}x${h}, not square: $path")
fi
done
if [ ${#problems[@]} -gt 0 ]; then
bad "listing copy: $manifest would be rejected by the portal"
printf ' %s\n' "${problems[@]}"
return 1
fi
ok "listing copy: shortDescription is ${#short}/30 chars, required interface fields present, both icons square"
}
# chafe: the Claude directory submission (2026-08-08) went out with five starter
# prompts, three of which appeared nowhere in README.md — "verified" by grepping
# the README *and* skills/ together and reading the hit as a README hit. A
# listing that promises a phrase the docs never use sends the curious user's
# first attempt into nothing, which is the exact moment adoption is won or lost.
# Second chafe, 2026-08-10: this check could never have caught that. `missing`
# was declared, tested, and never appended to — the drift branch was dead code,
# and the portal's prompt list had no repo-side source of truth to diff against.
# A sixth prompt typed straight into the form proved it, passing green.
check_starter_prompts() {
local doc=README.md manifest=.codex-plugin/plugin.json
local listing=docs/codex-submission.md
local prompt missing=() found=0 listed=0
# The README block is the source of truth; the manifest's defaultPrompt must
# be one of them. Bounded to a named list so this stays a token scan, not a
# prose parser — same shape as the glossary and threshold checks.
while read -r prompt; do
[ -n "$prompt" ] || continue
found=$((found + 1))
done < <(sed -n 's/^- "\(.*\)"$/\1/p' "$doc")
# What actually gets typed into the portal now lives in $listing, because a
# human types it and nothing else in this repo knew what they typed. Every
# prompt promised there must exist verbatim in the README.
# shellcheck disable=SC2016 # backticks are markdown code spans, not a subshell
local row='s/^| `\(.*\)` |.*$/\1/p'
while read -r prompt; do
[ -n "$prompt" ] || continue
listed=$((listed + 1))
grep -qF -- "- \"$prompt\"" "$doc" || missing+=("$prompt")
done < <(sed -n "$row" "$listing")
if [ "$listed" -lt 3 ]; then
bad "starter prompts: found $listed prompts in $listing — the listing scan is broken"
return 1
fi
if [ "$listed" -ne "$found" ]; then
bad "starter prompts: $listing lists $listed, $doc lists $found — the portal and the repo disagree"
return 1
fi
# A scan that finds nothing passes vacuously — the trap check_chafe_comments
# and check_footprint_manifest both guard against. Insist it read the block.
if [ "$found" -lt 3 ]; then
bad "starter prompts: found $found quoted prompts in $doc — the scan is broken"
return 1
fi
if [ ${#missing[@]} -gt 0 ]; then
bad "starter prompts: listed in the directories but absent from $doc: ${missing[*]}"
return 1
fi
local default
default=$(jq -r '.interface.defaultPrompt // ""' "$manifest")
if [ -n "$default" ] && ! grep -qF "\"$default\"" "$doc"; then
bad "starter prompts: $manifest defaultPrompt is not in $doc verbatim: \"$default\""
return 1
fi
ok "starter prompts: $found in $doc, all $listed in $listing match, defaultPrompt is one of them"
}
# chafe: branch-protection.sh shipped with die() called before its definition
# and nothing caught it (fixed in the Phase 0 PR). shellcheck catches that whole
# class. Skipped, loudly, when the tool is absent — a check that quietly does
# nothing on a laptop is worse than one that says it didn't run.
check_shell_lint() {
local scripts=(templates/stack/branch-protection.sh tests/check.sh install.sh)
if ! command -v shellcheck > /dev/null 2>&1; then
ok "shell lint: SKIPPED — shellcheck not installed (CI installs it)"
return 0
fi
if shellcheck "${scripts[@]}"; then
ok "shell lint: ${#scripts[@]} scripts clean"
else
bad "shell lint: shellcheck found problems (above)"
fi
}
# chafe: none yet — okrdev ships three workflow templates adopters run in their
# own repos, and until Phase 0 nothing in this repo had ever executed or parsed
# them. actionlint also runs shellcheck over each `run:` block.
check_workflow_lint() {
local workflows=(.github/workflows/check.yml templates/github/workflows/ci.yml
templates/github/workflows/neon-cleanup.yml templates/github/workflows/okr-gate.yml)
if ! command -v actionlint > /dev/null 2>&1; then
ok "workflow lint: SKIPPED — actionlint not installed (CI installs it)"
return 0
fi
if actionlint "${workflows[@]}"; then
ok "workflow lint: ${#workflows[@]} workflows clean"
else
bad "workflow lint: actionlint found problems (above)"
fi
}
# chafe: templates/stack/README.md ships a copy-pasteable e2e-preview workflow
# that interpolates github.event.deployment.ref straight into a run: block, on a
# deployment_status trigger holding NEON_API_KEY. A branch name carrying a quote
# breaks out and runs arbitrary commands with the secrets in scope; git accepts
# such names. actionlint does not flag it. Found in Phase 1 recon.
check_workflow_injection() {
# GitHub expands ${{ }} textually before bash sees the script, so attacker-
# controllable context inside a run: body is a command-injection hole. The
# fix is always the same: bind it to env: and let bash read a variable.
# shellcheck disable=SC2016 # awk program: $0 and ${{ }} must stay literal
local scanner='
function indent(s) { match(s, /^ */); return RLENGTH }
function flag() { if ($0 ~ /\$\{\{[ ]*github\.(event|head_ref)/) print FILENAME ":" FNR ": " $0 }
BEGIN { active = (md ? 0 : 1) }
md && /^```ya?ml$/ { active = 1; in_run = 0; next }
md && /^```/ { active = 0; in_run = 0; next }
!active { next }
/^[ \t]*(- +)?run:/ { run_col = index($0, "run:") - 1; in_run = 1; flag(); next }
{ if (in_run) {
if ($0 ~ /^[ \t]*$/) next
if (indent($0) <= run_col) { in_run = 0; next }
flag()
} }'
local hits
hits=$( { awk -v md=0 "$scanner" .github/workflows/*.yml templates/github/workflows/*.yml
awk -v md=1 "$scanner" templates/stack/README.md docs/*.md; } 2>/dev/null )
if [ -z "$hits" ]; then
ok "workflow injection: no untrusted github context inside a run: body"
else
bad "workflow injection: attacker-controllable context expanded into a shell"
printf '%s\n' "$hits" | sed 's/^/ /'
printf ' %s\n' "fix: bind it to env: and reference the variable from bash"
fi
}
# chafe: docs/live-coached-sessions.md ships a coach-monitor.sh using `stat -f%z`,
# which is BSD-only. On Linux it errors, `|| echo 0` swallows it, and the watcher
# loops forever emitting nothing — a rig that looks armed and transmits nothing.
# The linters pass it clean; only running it on Linux finds this. Phase 1 recon.
check_embedded_script_portability() {
# Scripts pasted out of docs run on whatever laptop the reader has. A
# platform-only builtin that fails into a fallback is the worst shape of
# bug: no crash, no warning, just silence.
local hits
hits=$(grep -rn -- 'stat -f%' docs/ templates/ skills/ install.sh 2>/dev/null)
if [ -z "$hits" ]; then
ok "embedded scripts: no BSD-only stat syntax"
else
bad "embedded scripts: BSD-only \`stat -f%\` fails silently on Linux"
printf '%s\n' "$hits" | sed 's/^/ /'
printf ' %s\n' 'fix: use wc -c, which is POSIX and needs no stat dialect'
fi
}
# chafe: the gate states the KR grammar a FOURTH time at okr-gate.yml:252, as an
# id re-parse, and check_kr_grammar cannot see it — it greps `^KR:`. The two are
# coupled by an unguarded `idMatch[2]`, so a one-sided edit throws a TypeError on
# every PR instead of warning. Found in Phase 1 recon.
check_gate_grammar_tests() {
local passed
# The reporter is pinned. node picks it from whether stdout is a TTY, and
# the default for a redirected run changed between node 22 and 24 — so this
# counted 7 on CI and 0 on a laptop, from the same command. A count that
# depends on the runtime's version is not a count.
if node --test --test-reporter=tap tests/gate-grammar.test.js > /tmp/okrdev-gate-test.log 2>&1; then
passed=$(grep -c '^ok ' /tmp/okrdev-gate-test.log)
# "Exit 0" and "tests ran" are different claims. node picks its reporter
# from whether stdout is a TTY, so redirecting the run can change the
# output format out from under the count and leave it reporting zero
# passes next to the word "pass" — green, and meaningless.
if [ "$passed" -lt 1 ]; then
bad "okr-gate grammar: node --test exited 0 but $passed tests were counted — nothing was verified"
printf ' %s\n' "the reporter's output format is not what the count expects:"
head -3 /tmp/okrdev-gate-test.log | sed 's/^/ /'
else
ok "okr-gate grammar: $passed unit tests pass"
fi
else
bad "okr-gate grammar: unit tests failed"
grep -E '^not ok|Error|assert' /tmp/okrdev-gate-test.log | head -12 | sed 's/^/ /'
fi
rm -f /tmp/okrdev-gate-test.log
}
# chafe: none yet — the gate is the only real code okrdev ships, and a syntax
# error in it would surface first in an adopter's Actions log.
check_gate_js_syntax() {
local yml=templates/github/workflows/okr-gate.yml
local body tmp
# github-script evaluates the body inside an AsyncFunction, so its top-level
# `await` and `return` are legal there and nowhere else. Re-wrap to match.
body=$(sed -n '/^ script: |$/,$p' "$yml" | tail -n +2 | sed 's/^ //')
# `mktemp -d` plus a fixed name, not `mktemp --suffix` — the latter is
# GNU-only, and when it failed on macOS $tmp became empty, the redirect below
# failed, and `node --check ""` exited 0. The check reported "ok" having
# parsed nothing. check_gnu_only_syntax now guards the whole class.
local dir
dir=$(mktemp -d) || { bad "okr-gate: could not create a temp dir"; return 1; }
tmp=$dir/gate.js
{ printf 'async function gate(github, core, context, process) {\n'
printf '%s\n' "$body"
printf '}\n'; } > "$tmp"
if [ ! -s "$tmp" ]; then
bad "okr-gate: extracted an empty script body from $yml — the extractor is broken"
rm -rf "$dir"
return 1
fi
if node --check "$tmp" 2> /dev/null; then
ok "okr-gate: embedded script parses"
else
bad "okr-gate: embedded script has a syntax error"
node --check "$tmp" 2>&1 | sed 's/^/ /'
fi
rm -rf "$dir"
}
# chafe: skills/triage/SKILL.md shipped with `to a decision: promote` — a bare
# colon-space inside an unquoted scalar, which closes the plain scalar and makes
# a strict YAML parser read a nested mapping. Claude Code's lenient loader took
# it, so it reached the Claude directory unnoticed; OpenAI's portal rejected the
# upload with "Malformed skill frontmatter YAML" and named the line. 38 checks
# and not one of them had ever opened a SKILL.md's frontmatter with a parser.
check_skill_frontmatter() {
# No YAML parser is available — this repo runs on jq and node, and adding a
# dependency to validate two keys is the wrong trade. So this validates the
# narrow format okrdev actually ships (a flat mapping of single-line scalars)
# and REJECTS anything it cannot prove a strict parser will accept. Stricter
# than YAML on purpose: a false alarm costs a quoted string, a miss costs a
# rejected submission.
local dir tmp
dir=$(mktemp -d) || { bad "skill frontmatter: could not create a temp dir"; return 1; }
tmp=$dir/frontmatter.js
cat > "$tmp" <<'NODE'
const fs = require('fs'), path = require('path');
const dirs = fs.readdirSync('skills', {withFileTypes: true})
.filter(d => d.isDirectory()).map(d => d.name).sort();
const problems = [];
let scanned = 0;
// A leading YAML indicator makes a plain scalar mean something other than text.
const INDICATORS = ['[', ']', '{', '}', '&', '*', '!', '|', '>', '%', '@', '`', '#', ',', '?', '-', ':'];
for (const name of dirs) {
const file = path.join('skills', name, 'SKILL.md');
if (!fs.existsSync(file)) { problems.push(`${file}: no SKILL.md`); continue; }
const lines = fs.readFileSync(file, 'utf8').split('\n');
if (lines[0] !== '---') { problems.push(`${file}:1: does not open with ---`); continue; }
const end = lines.indexOf('---', 1);
if (end === -1) { problems.push(`${file}: frontmatter is never closed`); continue; }
const body = lines.slice(1, end);
if (body.length === 0) { problems.push(`${file}: frontmatter is empty`); continue; }
scanned++;
const keys = new Map();
body.forEach((line, i) => {
const n = i + 2; // 1-based, past the opening ---
if (line.includes('\t')) problems.push(`${file}:${n}: contains a tab`);
const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*): (.*)$/);
if (!m) { problems.push(`${file}:${n}: not a flat "key: value" line — ${JSON.stringify(line)}`); return; }
const [, key, value] = m;
if (keys.has(key)) problems.push(`${file}:${n}: duplicate key "${key}"`);
keys.set(key, value);
if (value === '') { problems.push(`${file}:${n}: "${key}" has an empty value`); return; }
const q = value[0];
if (q === '"' || q === "'") {
if (value.length < 2 || value[value.length - 1] !== q) {
problems.push(`${file}:${n}: "${key}" opens with ${q} and never closes`);
}
return; // a closed quoted scalar may hold anything
}
// Everything below is why the triage line failed.
const colon = value.indexOf(': ');
if (colon !== -1) {
problems.push(`${file}:${n}: "${key}" is unquoted and contains ": " at column ${colon + key.length + 3}` +
` — a strict parser reads a nested mapping here. Quote the value or reword.`);
}
if (value.endsWith(':')) problems.push(`${file}:${n}: "${key}" is unquoted and ends with ":"`);
if (INDICATORS.includes(value[0])) {
problems.push(`${file}:${n}: "${key}" is unquoted and starts with the YAML indicator "${value[0]}"`);
}
if (value.includes(' #')) problems.push(`${file}:${n}: "${key}" is unquoted and contains " #" — the rest parses as a comment`);
});
for (const required of ['name', 'description']) {
if (!keys.has(required)) problems.push(`${file}: frontmatter has no "${required}"`);
}
// The directory is the invocation name; frontmatter disagreeing with it is
// the same class of two-copies-no-assert drift the manifests check guards.
if (keys.has('name') && keys.get('name') !== name) {
problems.push(`${file}: frontmatter name "${keys.get('name')}" != directory "${name}"`);
}
}
console.log(`SCANNED:${scanned}`);
problems.forEach(p => console.log(`PROBLEM:${p}`));
NODE
local out scanned
if ! out=$(node "$tmp" 2>&1); then
bad "skill frontmatter: the validator itself failed"
printf '%s\n' "$out" | sed 's/^/ /'
rm -rf "$dir"
return 1
fi
rm -rf "$dir"
scanned=$(printf '%s\n' "$out" | sed -n 's/^SCANNED://p')
# A scan that reads nothing passes vacuously — the same trap check_chafe_comments
# sets for itself, and the same one `node --check ""` fell into here once.
if [ -z "$scanned" ] || [ "$scanned" -lt 2 ]; then
bad "skill frontmatter: scanned ${scanned:-0} skills — the scan itself is broken"
return 1
fi
if printf '%s\n' "$out" | grep -q '^PROBLEM:'; then
bad "skill frontmatter: malformed YAML would be rejected by a strict parser"
printf '%s\n' "$out" | sed -n 's/^PROBLEM:/ /p'
return 1
fi
ok "skill frontmatter: $scanned skills parse as strict flat YAML, names match their directories"
}
# chafe: none yet — /okrdev:uninstall is documented but unimplemented (parked,
# next-cycle candidate), which is the shape of the mistake this catches.
check_skill_references() {
# Two names resolve to nothing and are correct today. Both are claims about
# skills that do not exist: one promised, one refused.
local exempt="uninstall test" # uninstall: promoted, not yet built. test: refused in docs/testing.md.
local name missing=()
while read -r name; do
case " $exempt " in *" $name "*) continue ;; esac
[ -f "skills/$name/SKILL.md" ] || missing+=("$name")
done < <(grep -rhoE '/okrdev:[a-z-]+' docs/ skills/ templates/ README.md 2>/dev/null | sed 's|/okrdev:||' | sort -u)
if [ ${#missing[@]} -eq 0 ]; then
ok "skill references: every /okrdev:<skill> resolves (exempt: $exempt)"
else
bad "skill references: no SKILL.md for ${missing[*]}"
fi
}
# chafe: BOTH defects this suite caught in Phase 1 part 2 hid in the gap between
# a laptop and CI — GNU vs BSD `mktemp`, and node 22 vs 24 changing `node --test`'s
# default reporter out from under a count. A local run you cannot trust sends you
# to Actions to find out, which is the slow loop this repo claims to refuse.
check_local_loop() {
local ci_node local_node hooks notes=()
ci_node=$(sed -n "s/.*node-version: *'\{0,1\}\([0-9]*\).*/\1/p" .github/workflows/check.yml | head -1)
local_node=$(node --version 2>/dev/null | sed 's/^v\([0-9]*\).*/\1/')
if [ -n "$ci_node" ] && [ -n "$local_node" ] && [ "$ci_node" != "$local_node" ]; then
notes+=("node ${local_node} here vs ${ci_node} on CI — the two disagreed about \`node --test\` output once already")
fi
# Not on CI: the hook is a laptop concern, and CI has no laptop.
if [ -z "${CI:-}" ]; then
hooks=$(git config --get core.hooksPath 2>/dev/null)
[ "$hooks" = "tests/hooks" ] || notes+=("pre-push hook not wired — \`git config core.hooksPath tests/hooks\`")
fi
command -v shellcheck > /dev/null 2>&1 || notes+=("shellcheck missing — its check is skipping, not passing")
command -v actionlint > /dev/null 2>&1 || notes+=("actionlint missing — its check is skipping, not passing")
# Advisory on purpose. A contributor on the wrong node should be told, not
# blocked — but never left to assume a green run here means a green run there.
if [ ${#notes[@]} -eq 0 ]; then
ok "local loop: matches CI — a green run here means a green run there"
else
ok "local loop: ADVISORY — this run is weaker than CI will be"
printf ' - %s\n' "${notes[@]}"
fi
}
# chafe: this suite's own admission rule, made mechanical — the plan promises
# growth only from chafe, and a promise nothing checks is theater.
check_chafe_comments() {
local undocumented=() found=0 line
# Walk the file, remembering whether the comment block sitting directly
# above each check named a chafe. Anything else — code, a blank line —
# ends the block, so a chafe line can only vouch for the check it precedes.
while read -r line; do
case "$line" in
TOTAL:*) found=${line#TOTAL:} ;;
*) undocumented+=("$line") ;;
esac
done < <(awk '
/^# chafe:/ { chafe = 1; next }
/^#/ { next }
/^check_[a-z_]+\(\) \{/ { total++; name = $1; sub(/\(\)$/, "", name)
if (!chafe) print name
chafe = 0; next }
{ chafe = 0 }
END { print "TOTAL:" total + 0 }
' "${BASH_SOURCE[0]}")
# A scan that finds nothing passes vacuously — the exact failure this file
# exists to catch. Insist it found the other checks before believing it.
if [ "$found" -lt 2 ]; then
bad "chafe comments: scanned $found checks — the scan itself is broken"
elif [ ${#undocumented[@]} -eq 0 ]; then
ok "all $found checks name the chafe that motivated them"
else
bad "checks with no '# chafe:' line: ${undocumented[*]}"
fi
}
# chafe: the cycle file and the check-ins state each KR's confidence twice, and
# nothing kept them equal. Live near-miss found writing this check: the naive
# "newest check-in by filename" reads 2026-W32 — a file created only to log a
# judgment call, with a header-only confidence table — and passes vacuously
# while the real numbers came from W31. Handoff issue #11, item (a).
check_confidence_mirror() {
local corpora=(okrdev examples/acme-fitness)
local root cycle_file cycle checkin report status=0
for root in "${corpora[@]}"; do
local active
active=$(grep -l '^status: active' "$root"/okrs/*.md 2> /dev/null)
if [ -z "$active" ]; then
bad "confidence mirror: $root has no cycle file with 'status: active'"
status=1
continue
fi
# At most one active cycle — docs/testing.md names two-actives as a fixture
# the suite must reject. Taking the first match would pick a winner and
# report green, which is how the invariant would get lost.
if [ "$(printf '%s\n' "$active" | wc -l | tr -d ' ')" -ne 1 ]; then
bad "confidence mirror: $root has more than one cycle with 'status: active'"
printf '%s\n' "$active" | sed 's/^/ /'
status=1
continue
fi
cycle_file=$active
cycle=$(basename "$cycle_file" .md)
# "Newest" is a lexical compare, which is only chronological while every
# filename is fixed-width: 2026-W9.md sorts after 2026-W28.md and would
# silently select the wrong check-in. `date +%G-W%V` zero-pads, so this can
# only arrive by hand — which is exactly when a silent wrong answer is
# worst. Refuse the input instead of mis-sorting it.
local misnamed
misnamed=$(find "$root/checkins/$cycle" -name '*.md' 2> /dev/null |
grep -vE '/[0-9]{4}-W[0-9]{2}\.md$')
if [ -n "$misnamed" ]; then
bad "confidence mirror: check-in filenames must be <yyyy>-W<nn>.md or 'newest' is not chronological"
printf '%s\n' "$misnamed" | sed 's/^/ /'
status=1
continue
fi
checkin=$(newest_held_checkin "$root/checkins/$cycle")
if [ -z "$checkin" ]; then
# Not "no check-ins yet, fine" — a live cycle whose confidences are
# mirrored nowhere is the state this check exists to notice.
bad "confidence mirror: $root/checkins/$cycle has no held check-in (no table with KR rows)"
status=1
continue
fi
report=$(awk -v cyc="$cycle_file" -v chk="$checkin" '
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
BEGIN {
while ((getline line < cyc) > 0) {
if (line ~ /^## KR[0-9]+\.[0-9]+:/) { split(line, a, ":"); id = trim(substr(a[1], 4)); cur = id }
else if (cur != "" && line ~ /^Status:[ \t]*dropped/) { dropped[cur] = 1 }
else if (id != "" && line ~ /^Confidence:/) {
# Strip a trailing comment before trimming: the cycle TEMPLATE
# ships `Confidence: 0.5 # a good stretch KR is a coin flip`, so a
# DRI who keeps the hint would otherwise compare the whole line.
v = line; sub(/^Confidence:/, "", v); sub(/#.*$/, "", v)
want[id] = trim(v); order[++n] = id; id = ""
}
}
while ((getline line < chk) > 0) {
if (line ~ /^\|[ \t]*KR[0-9]/) {
split(line, f, "|"); k = trim(f[2]); got[k] = trim(f[5]); rows++
}
}
# Both sides must have found something. A parser that silently matched
# nothing would report "all mirrored" over two empty sets.
if (n == 0) { print "PARSE|no KR headings with a Confidence: line in " cyc; exit }
if (rows == 0) { print "PARSE|no KR rows parsed out of " chk; exit }
for (i = 1; i <= n; i++) {
id = order[i]
# A dropped KR is CHECKED whenever it appears — it still carries a
# confidence and is the one most likely to rot — but it is not
# REQUIRED to appear, because skills/checkin/SKILL.md tells the coach
# "one row per KR (skip Status: dropped ones)". Requiring it would go
# red on a correctly written check-in; requiring nothing would stop
# checking it at all. Asserting over the intersection decides neither
# reading, which is the rule for doctrine the docs have not settled.
# The acme KR1.2 row is dropped and present in W36, so this path has
# live cover today.
if (!(id in got)) {
if (!(id in dropped)) print "MISS|" id " has Confidence: " want[id] " but no row in the check-in"
}
else if (got[id] != want[id]) print "DIFF|" id " cycle says " want[id] ", check-in Now says " got[id]
seen[id] = 1
}
for (k in got) if (!(k in seen)) print "EXTRA|" k " has a check-in row but no KR in the cycle file"
print "COUNT|" n
}')
# The count line is always emitted, so "a COUNT line exists" is not the
# success condition — the absence of problem lines is. Getting this
# backwards made the check report "5 KRs match" over a deliberately
# corrupted table; caught by mutation-testing it, which is why the doctrine
# says to.
local problems count
problems=$(printf '%s\n' "$report" | grep -v '^COUNT|')
count=$(printf '%s\n' "$report" | sed -n 's/^COUNT|//p')
if [ -n "$count" ] && [ -z "$problems" ]; then
ok "confidence mirror: $root — $count KRs match $(basename "$checkin")"
else
bad "confidence mirror: $root disagrees with $(basename "$checkin")"
printf '%s\n' "$problems" | sed 's/^[A-Z]*|/ /'
status=1
fi
done
return $status
}
# chafe: docs/adoption.md listed a cycle file under Level 1's "What installs"
# while skills/install/SKILL.md says in bold not to create one. That
# contradiction sat on main and had to be escalated to the DRI by hand
# (handoff issue #11, decision 1) — a reviewer-class coherence breach of
# exactly the kind the cycle's health metric names, and nothing mechanical
# could see it. The manifest is what a check can read.
check_footprint_manifest() {
local manifest=tests/install-footprint.md
local skill=skills/install/SKILL.md
local t missing=() outside=() unlisted=() phantom=() all copied refs status=0
[ -f "$manifest" ] || { bad "footprint: $manifest is missing"; return 1; }
# shellcheck disable=SC2016 # backticks are markdown code spans, not a subshell
local span='`templates/[A-Za-z0-9._/-]+`'
all=$(grep -oE "$span" "$manifest" | tr -d '`' | sort -u)
# Table A only — the per-level install list. Scoping matters: the manifest
# also carries a table of templates install deliberately does NOT copy, and
# comparing against the whole document would make the next assert vacuous by
# construction (every template listed somewhere ⇒ nothing can ever be missing).
copied=$(sed -n '/^| Level | Destination/,/^$/p' "$manifest" | grep -oE "$span" | tr -d '`' | sort -u)
refs=$(grep -oE "$span" "$skill" | tr -d '`' | sort -u)
if [ -z "$all" ] || [ -z "$copied" ] || [ -z "$refs" ]; then
bad "footprint: scan found nothing (manifest=$(printf '%s' "$all" | grep -c .) copied=$(printf '%s' "$copied" | grep -c .) skill=$(printf '%s' "$refs" | grep -c .)) — the scan itself is broken"
return 1
fi
# Every template the manifest names must exist. A manifest that points at a
# deleted template is worse than none: it reports green about a broken install.
while read -r t; do
[ -n "$t" ] || continue
[ -e "$t" ] || missing+=("$t")
done <<< "$all"
# Every template the install skill reaches for must be accounted for — either
# as a copy (table A) or as a deliberate non-copy (table B). This is the half
# that catches install GROWING a write nobody wrote down.
while read -r t; do
[ -n "$t" ] || continue
printf '%s\n' "$all" | grep -qxF "$t" || unlisted+=("$t")
done <<< "$refs"
# And the other direction: the manifest may not claim a copy install never
# makes. Without this the manifest could drift into wishful thinking and
# still pass everything above.
while read -r t; do
[ -n "$t" ] || continue
printf '%s\n' "$refs" | grep -qxF "$t" || phantom+=("$t")
done <<< "$copied"
# The health metric's red line, mechanized: okrdev/, the marked CLAUDE.md
# block, and .github/ at Level 2. Anything else is a breach by definition.
while read -r t; do
[ -n "$t" ] || continue
case "$t" in okrdev/* | CLAUDE.md | AGENTS.md | .github/*) ;; *) outside+=("$t") ;; esac
done < <(sed -n '/^| Level | Destination/,/^$/p' "$manifest" |
awk -F'|' 'NR > 2 { gsub(/^[ \t]+|[ \t]+$/, "", $3); sub(/ \(marked block only\)$/, "", $3)
gsub(/`/, "", $3); if ($3 != "" && $3 != "—") print $3 }' | sort -u)
if [ ${#missing[@]} -gt 0 ]; then
bad "footprint: manifest names templates that do not exist: ${missing[*]}"
status=1
fi
if [ ${#unlisted[@]} -gt 0 ]; then
bad "footprint: $skill uses templates the manifest does not list: ${unlisted[*]}"
status=1
fi
if [ ${#phantom[@]} -gt 0 ]; then
bad "footprint: the manifest claims installs $skill never makes: ${phantom[*]}"
status=1
fi
if [ ${#outside[@]} -gt 0 ]; then
bad "footprint: destinations outside the red line (okrdev/, CLAUDE.md, AGENTS.md, .github/): ${outside[*]}"
status=1
fi
[ $status -eq 0 ] && ok "footprint: $(printf '%s' "$copied" | grep -c .) templates copied by install, $(printf '%s' "$all" | grep -c .) accounted for, every destination inside the red line"
return $status
}
# chafe: docs/adoption.md's Level 1 "What installs" listed a cycle file and a
# checkins/ directory that install has never written — the contradiction that
# blocked the footprint manifest and had to go to the DRI as a ruling (handoff
# issue #11, decision 1). The manifest alone would not have caught it: nothing
# compared the adopter-facing description to the install skill. This does.
check_adoption_install_list() {
local doc=docs/adoption.md manifest=tests/install-footprint.md
local dests claimed p missing=() found=0
# The manifest's destination column is the source of truth: it is checked
# against the install skill in both directions by check_footprint_manifest,
# so agreeing with it means agreeing with what install actually does.
dests=$(sed -n '/^| Level | Destination/,/^$/p' "$manifest" |
awk -F'|' 'NR > 2 { gsub(/^[ \t]+|[ \t]+$/, "", $3); sub(/ \(marked block only\)$/, "", $3)
gsub(/`/, "", $3); if ($3 != "" && $3 != "—") print $3 }' | sort -u)
# Every repo path adoption.md promises a level installs. Bounded on purpose:
# only backticked spans that name a destination inside the red line, only
# inside a "**What installs:**" block. Not a prose parser — a token scan over
# one named paragraph, the same shape as the glossary row count.
# The block ends at the next bolded lead-in or the next heading, NOT at the
# next blank line: Level 2 states its installs as a bulleted list, and a
# blank-line terminator would silently read only its first sentence.
# shellcheck disable=SC2016 # backticks are markdown code spans, not a subshell
local dest_span='`(okrdev|\.github)/[A-Za-z0-9._<>/-]*`|`CLAUDE\.md`|`AGENTS\.md`'
claimed=$(awk '/^\*\*What installs:\*\*/ { inblock = 1; print; next }
/^\*\*/ || /^#{2,3} / { inblock = 0 }
inblock' "$doc" |
grep -oE "$dest_span" | tr -d '`' | sort -u)
if [ -z "$claimed" ]; then
bad "adoption list: no installed paths found in $doc — the scan is broken"
return 1
fi
while read -r p; do
[ -n "$p" ] || continue
found=$((found + 1))
printf '%s\n' "$dests" | grep -qxF "$p" || missing+=("$p")
done <<< "$claimed"
if [ ${#missing[@]} -eq 0 ]; then
ok "adoption list: all $found paths $doc promises are ones install actually writes"
else
bad "adoption list: $doc promises paths install never writes: ${missing[*]}"
printf ' %s\n' "either $doc is wrong, or the install skill and $manifest are — one of them has to give"
fi
}
# chafe: none yet — green on arrival, so it was mutation-tested instead (break
# one number, watch it fail, put it back). Install copies the config template
# verbatim and never sets the version, so a skew here makes every adopter
# record a version they don't have, silently. Handoff issue #11, item (c).
check_version_sync() {
# Deliberately NOT asserted here: examples/acme-fitness, which records what a
# fictional adopter installed rather than what ships, and is left on an older
# marker on purpose. okrdev's OWN okrdev/config.md is asserted — by
# check_dogfood_current, not by this check — because a release is not done
# until okrdev has upgraded itself. This comment previously claimed that file
# was unasserted and pinned it at 0.1.0; both halves were wrong, and stale
# comments about what is checked are how a suite starts lying about itself.
local tmpl_version plugin_version
tmpl_version=$(sed -n 's/^okrdev_version:[[:space:]]*\([0-9.]*\).*/\1/p' templates/okrdev/config.md | head -1)
plugin_version=$(jq -r '.version' .claude-plugin/plugin.json)
if [ -z "$tmpl_version" ]; then
bad "version sync: no okrdev_version found in templates/okrdev/config.md"
elif [ "$tmpl_version" = "$plugin_version" ]; then
ok "version sync: template config and plugin.json both say $plugin_version"
else
bad "version sync: templates/okrdev/config.md says $tmpl_version, plugin.json says $plugin_version"
printf ' %s\n' "policy: any file okrdev puts in your repo changes → bump both markers together (docs/adoption.md § Upgrading)"
fi
}
# chafe: this repo's own install sat at 0.1.0 while it shipped 0.4.0 — three
# releases of its own scaffolding that it had never run. Captured 2026-08-04 in
# the parking lot's sync-wrinkles entry and still unfixed on 2026-08-07, which
# is the parked-but-unfixed gap docs/testing.md says a required check closes.
# The dogfood claim in okrdev/config.md ("okrdev runs on itself") is only true
# if it runs on the version it ships.
check_dogfood_current() {
# okrdev's OWN install only. examples/acme-fitness is fiction and is
# deliberately left on an old marker — a worked example of an adopter who
# has not upgraded is worth more than one that is magically current.
local live shipped
live=$(sed -n 's/^okrdev_version:[[:space:]]*\([0-9.]*\).*/\1/p' okrdev/config.md | head -1)
shipped=$(jq -r '.version' .claude-plugin/plugin.json)
if [ -z "$live" ]; then
bad "dogfood: no okrdev_version in okrdev/config.md"
elif [ "$live" = "$shipped" ]; then
ok "dogfood: this repo runs the okrdev it ships ($shipped)"
else
bad "dogfood: this repo runs okrdev $live but ships $shipped"
printf ' %s\n' "a release is not done until okrdev has upgraded itself (docs/adoption.md § Upgrading)"
printf ' %s\n' "run the upgrade — coach block, any okrdev-installed .github/ files, config keys — then move the marker"
fi
}
# chafe: docs/shipping-explained.md opens by promising "one story, ten words,
# one table" over a glossary that has held eleven rows since the demo row
# landed. Found while specifying the adopter prescription (docs/testing.md).
check_glossary_promise() {
local doc=docs/shipping-explained.md
local promise word rows n
# Unwrapped first: the sentence is hard-wrapped at 95 columns and today the
# break falls between "one" and "story", so a line-oriented grep sees neither
# half. A check that can be defeated by a reflow is a check that will be.
promise=$(tr '\n' ' ' < "$doc" | grep -oE 'one story, [a-z]+ words, one table' | head -1)
if [ -z "$promise" ]; then
bad "glossary promise: the 'one story, N words, one table' sentence is gone from $doc"
return 1
fi
word=${promise#one story, }
word=${word% words, one table}
case "$word" in
seven) n=7 ;; eight) n=8 ;; nine) n=9 ;; ten) n=10 ;; eleven) n=11 ;;
twelve) n=12 ;; thirteen) n=13 ;; fourteen) n=14 ;; fifteen) n=15 ;;
*) bad "glossary promise: '$word' is not a number word this check knows — add it"; return 1 ;;
esac
rows=$(grep -c '^| \*\*' "$doc")
if [ "$rows" -eq "$n" ]; then
ok "glossary promise: $doc promises $word words and defines $rows"
else
bad "glossary promise: $doc promises $word ($n) words, the glossary defines $rows"
printf ' %s\n' "fix whichever is wrong — the sentence at the top, or the table"
fi
}
# chafe: the same number is stated in up to six files and the copies have to
# move together, but only where a single literal covers every legitimate use.
# The three thresholds whose phrasing AND numeral both vary are in
# DO_NOT_FREEZE instead — see the entries below. Handoff issue #11, item (d).
check_threshold_tokens() {
local staleness="CLAUDE.md templates/CLAUDE-okrdev.md docs/ai-coach.md docs/rituals.md skills/coach/SKILL.md skills/checkin/SKILL.md"
local share="docs/method.md docs/rituals.md docs/ai-coach.md skills/coach/SKILL.md skills/checkin/SKILL.md"
local sandbag="docs/method.md docs/ai-coach.md skills/coach/SKILL.md skills/checkin/SKILL.md skills/retro/SKILL.md"
# docs/testing.md quotes all four while explaining this very check. It states
# nothing — it describes — so it is exempt from the "and nowhere else" half
# everywhere, rather than being reworded to dodge its own greps.
local plan=docs/testing.md
local specs=(
"10 days|$staleness|$plan"
"30% of PRs|$share|$plan"
"5% of PRs|$share|$plan"
"60% of the cycle|$sandbag|$plan"
)
local spec token files exempt f status=0 msg anchored
for spec in "${specs[@]}"; do
IFS='|' read -r token files exempt <<< "$spec"
# Anchored on a non-digit, because a plain substring match is satisfied by
# a bigger number: `5% of PRs` is a substring of `15% of PRs`, so someone
# could change the threshold in one file and this check would still pass.
# Same shape for `10 days` inside `110 days`.
anchored="(^|[^0-9])$(printf '%s' "$token" | sed 's/[.[\*^$]/\\&/g')"
msg=""
for f in $files; do
grep -qE -- "$anchored" "$f" || msg+=" states-it-no-longer:$f"
done
# The other half: nowhere else may state it. A threshold that grows a
# seventh home is exactly how the copies start disagreeing, and a
# positive-only assert never notices.
while read -r f; do
case " $files $exempt " in *" $f "*) continue ;; esac
msg+=" also-states-it:$f"
done < <(grep -rlE --include='*.md' -- "$anchored" . 2> /dev/null | sed 's|^\./||' | sort)
if [ -z "$msg" ]; then
ok "threshold '$token': stated in all $(printf '%s' "$files" | wc -w | tr -d ' ') files, and nowhere else"
else
bad "threshold '$token' drifted:$msg"
status=1
fi
done
# Staleness only: the negative tokens. Scoped to the files that STATE the
# rule, never repo-wide — "9 days" is the canonical output-vs-outcome example
# in method.md and evidence.md and acme's KR1.2 target, so a repo-wide
# negative is permanently red on documents that are correct.
# The hyphenated spelling is live in the repo (acme's W28 says "9-day
# median"), so it is a form a future edit can reach for. Both are checked.
local near
for near in '9 days' '11 days' '9-day' '11-day'; do
for f in $staleness; do
if grep -qE -- "(^|[^0-9])$near" "$f"; then
bad "threshold: '$near' appears in $f, which states the 10-day rule"
status=1
fi
done
done
[ $status -eq 0 ] && ok "threshold: no near-miss staleness numbers in the 6 files that state the rule"
return $status
}
# chafe: check_gate_js_syntax used `mktemp --suffix=.js`, which is GNU-only. On
# macOS mktemp errors, $tmp is empty, the redirect fails, and `node --check ""`
# exits 0 — so the check printed "ok" having parsed nothing, on every laptop in
# the project. CI is ubuntu-latest, so CI never saw it. Found running this
# suite locally for handoff issue #11. Sibling of the BSD-only check above:
# same failure shape, opposite platform.
check_gnu_only_syntax() {
# Both directions matter now. This suite already refuses BSD-only syntax in
# scripts it ships to readers; it has to refuse GNU-only syntax in the
# scripts it runs itself, or "the checks passed" means "the checks passed on
# Linux" and nobody is told which.
# The pattern list matches itself, so it carries a sentinel and the results
# are filtered by it. Same shape as check_workflow_injection's awk program:
# a scanner that flags its own source teaches people to ignore the scanner.
local pattern='mktemp[^|;&]*--suffix|stat -c|date -[dr] |readlink -f|grep -P|base64 -w|find [^|;&]*-printf|head -n -|xargs -r' # portability-scanner
local hits
hits=$(grep -rnE "$pattern" tests/ templates/ docs/ skills/ install.sh 2> /dev/null |
grep -v 'portability-scanner' |
grep -vE '^[^:]*:[0-9]+:[[:space:]]*#') # comments describe these bugs; they don't run
if [ -z "$hits" ]; then
ok "portability: no GNU-only syntax in scripts this repo runs or ships"
else
bad "portability: GNU-only syntax fails on macOS/BSD — where most of this project is written"
printf '%s\n' "$hits" | sed 's/^/ /'
printf ' %s\n' 'fix: use the POSIX form, or one that works on both'
fi
}
# DO_NOT_FREEZE — "<what this suite must never assert>|<file>|<the divergent text>"
# Each entry asserts its divergence still EXISTS. When one stops being true,
# check_do_not_freeze fails and the entry is either promoted to a real rule
# through a doctrine PR, or deleted. The list cannot rot into a stale comment.
DO_NOT_FREEZE=(
"check-in health rows paraphrase the cycle's red lines|okrdev/checkins/2026-Q3/2026-W31.md|reviewer-class contradiction on main >1 week"
"thresholds share their number, never their phrasing|docs/ai-coach.md|more than 10 days old"
# The acme example ships a SAMPLED cycle — W28, W29, W36 standing in for ten
# weeks — so its Prev column deliberately does not chain. The anchor is the
# proof: W36 reads Prev 0.9 for KR2.1 while W29 left it at Now 0.75, because
# the weeks that moved it are not in the repo (W36 itself cites a W33 that
# does not exist). Backfilling check-ins to satisfy a chaining assert would
# be inventing evidence in the one directory that exists to teach people what
# evidence looks like. Handoff issue #11, item (f).
"the acme example's check-ins chain week to week|examples/acme-fitness/checkins/2026-Q3/2026-W36.md|| KR2.1 | jordan | 0.9 | 1.0 |"
# Three thresholds vary in phrasing AND numeral across their copies, so a
# token assert cannot cover them without picking a winner — which would be a
# doctrine decision made inside a test. Each entry pins one phrasing that
# must keep existing, so a future session cannot quietly normalize them into
# assertability without this list failing first. Handoff issue #11, item (d).
"the emergency ceiling's numeral: 'two' and '2' both live|docs/method.md|more than two a cycle"
"the flat-confidence window's numeral: 'three' and '3' both live|docs/method.md|three check-ins"
"4 box-hours means three different things|okrdev/config.md|side_quest_box_hours_per_week"
)
# chafe: this suite's other admission rule. A do-not-freeze list written as a
# comment is inert; written as negative tests it forces graduate-or-delete.
check_do_not_freeze() {
local entry what file text stale=()
for entry in "${DO_NOT_FREEZE[@]}"; do
IFS='|' read -r what file text <<< "$entry"
grep -qF -- "$text" "$file" || stale+=("$what ($file)")
done
if [ ${#stale[@]} -eq 0 ]; then
ok "do-not-freeze: ${#DO_NOT_FREEZE[@]} deliberate divergences still present"
else
bad "do-not-freeze entries no longer apply — graduate them or delete them:"
printf ' %s\n' "${stale[@]}"
fi
}
# ── Run ────────────────────────────────────────────────────────────────────
printf 'okrdev checks\n\n'
check_coach_block
check_branch_protection_bad_input
check_kr_grammar
check_judgment_call_format
check_plugin_manifests
check_listing_copy
check_starter_prompts
check_headless_install
check_payload
check_shell_lint
check_workflow_lint
check_workflow_injection
check_embedded_script_portability
check_gnu_only_syntax
check_gate_js_syntax
check_gate_grammar_tests
check_skill_frontmatter
check_skill_references
check_confidence_mirror
check_footprint_manifest
check_adoption_install_list
check_version_sync
check_dogfood_current
check_glossary_promise
check_threshold_tokens
check_local_loop
check_chafe_comments
check_do_not_freeze
printf '\n'
if [ "$failures" -eq 0 ]; then
printf 'all checks passed\n'
exit 0
fi
printf '%d check(s) failed\n' "$failures"
exit 1
SHA-256: b8529f40581b7c566e8d1ad0d93cd864064a6f12698d970ecfab509bc5c7aeac