diff --git a/clientdocs/guide-skill-coupling.txt b/clientdocs/guide-skill-coupling.txt new file mode 100644 index 0000000..bc6e610 --- /dev/null +++ b/clientdocs/guide-skill-coupling.txt @@ -0,0 +1,10 @@ +# clientdocs/guide-skill-coupling.txt -- guide<->skill coupling pin (L8). +# INTERNAL FILE: never shipped to a client. +# Pins the sha256 of the Jenkins + Kubernetes Implementation Guide. When the guide +# changes, repo lint FAILS until the tenant AI skill (tenant-skill/references/ +# kubernetes.md + troubleshooting.md) has been reviewed for the SAME update and this +# pin is re-recorded with: +# bash scripts/repo-lint.sh --record-guide-skill-coupling +# Rationale: guide edits repeatedly landed without mirroring into the skill +# (DOCFIX-135/136 skill-lag). This makes "sweep the skill" a hard gate, not a note. +b0dea4d001d14f9565aae607c21e38745336b66b2bafe8ba99f08fe7f35a5185 clientdocs/jenkins-kubernetes-guide.md diff --git a/docs/session-ledger.md b/docs/session-ledger.md index 8480307..e5c0736 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -472,6 +472,13 @@ (ingress stays recommendation+pointer by design). Harness 8/8, gauntlet 39/39, package re-instantiated. Committed + pushed. STANDING LESSON: guide changes must sweep the skill in the same change -- skill-lag caught twice (DOCFIX-135/136). +- **DONE -- DOCFIX-137 (addendum 53): the standing lesson is now ENFORCEABLE.** + repo-lint L8 guide<->skill coupling guard: clientdocs/guide-skill-coupling.txt + pins the guide's sha256; any guide change FAILS lint (naming the skill files to + review) until re-recorded with `bash scripts/repo-lint.sh + --record-guide-skill-coupling`. Harness +5 (28/28), gauntlet 39/39. So the next + guide edit CANNOT land without a conscious skill review. Skill-lag closed at the + source. - **S-CLASS BATCH STATUS:** DONE = S7, S4, S8a (DOCFIX-132/133/134). HELD/DEFERRED = S8b, S8c, and S1/S2/S3/S5 (lib-validate adoption -- until DOCFIX-126/127 live-verify). S6 was ruled no-action earlier. Next operator decision: S8b/S8c diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index 1a91124..73a3fd0 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -3387,4 +3387,28 @@ - Numbers consumed: DOCFIX-136. REVERT: git checkout HEAD~ -- clientdocs/tenant-skill/references/kubernetes.md clientdocs/tenant-skill/references/troubleshooting.md clientdocs/sweep-receipt.txt docs/v1-redeploy-changelog.md docs/session-ledger.md; then bash scripts/repo-lint.sh --record-clientdocs-sweep; re-instantiate the package skill files. -Next-free after this push (per scan, keep this line unwrapped): D-076, DOCFIX-137, BUNDLEFIX-012. +### 2026-07-08 (addendum 53, jumphost stream) -- DOCFIX-137: repo-lint L8 guide<->skill coupling guard (enforceable skill-lag gate) + +The tenant AI skill fell behind the Jenkins+K8s guide twice (DOCFIX-135 registry, +DOCFIX-136 six more topics) because guide edits shipped without mirroring into the +skill. Operator asked to make "sweep the skill on guide changes" ENFORCEABLE, not a +note. Added lint check L8, mirroring the L7 sweep-receipt pattern: +- clientdocs/guide-skill-coupling.txt pins the sha256 of + clientdocs/jenkins-kubernetes-guide.md (INTERNAL file, never shipped -- a .txt, so + not caught by L7's *.md/*.sh manifest). +- When the guide changes, repo-lint FAILS naming the skill files to review + (tenant-skill/references/kubernetes.md + troubleshooting.md), until the pin is + re-recorded: bash scripts/repo-lint.sh --record-guide-skill-coupling (rewrites ONLY + that pin). Skipped when the guide is absent. +- repo_lint.py: new COUPLING_FLAG + record block + L8 check; docstring L1-L7 -> L1-L8. + repo-lint.sh: the new flag rides the existing --* passthrough; runner comment updated. +- Harness tests/repo-lint/run-tests.sh +5 (missing pin FAILS, recorded PASSES, guide + drift FAILS with L7 re-recorded so L8 fires alone, re-record PASSES, no-guide + SKIPPED) -- 28/28. Gauntlet 39/39; repo-lint 0 fail. +- Calibration: like L7, the gate forces a conscious stop + review + explicit + re-record; it cannot cheaply PROVE the skill was edited, but it makes skipping the + review a deliberate act -- which is exactly what was missing. +- Numbers consumed: DOCFIX-137. +REVERT: git checkout HEAD~ -- scripts/repo_lint.py scripts/repo-lint.sh tests/repo-lint/run-tests.sh clientdocs/guide-skill-coupling.txt docs/v1-redeploy-changelog.md docs/session-ledger.md. + +Next-free after this push (per scan, keep this line unwrapped): D-076, DOCFIX-138, BUNDLEFIX-012. diff --git a/scripts/repo-lint.sh b/scripts/repo-lint.sh index 14133ea..e313aa6 100644 --- a/scripts/repo-lint.sh +++ b/scripts/repo-lint.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash -# scripts/repo-lint.sh [--record-clientdocs-sweep] [repo-root] +# scripts/repo-lint.sh [--record-clientdocs-sweep] [--record-guide-skill-coupling] [repo-root] # # Thin runner for scripts/repo_lint.py (DOCFIX-074) -- static repo hygiene lint. -# Mutates NOTHING -- except under --record-clientdocs-sweep (DOCFIX-104), which -# rewrites ONLY clientdocs/sweep-receipt.txt (the L7 sweep marker) and then -# lints as usual. See the .py docstring for the check catalog (L1-L7). +# Mutates NOTHING -- except under --record-clientdocs-sweep (DOCFIX-104, rewrites +# ONLY clientdocs/sweep-receipt.txt) or --record-guide-skill-coupling (rewrites +# ONLY clientdocs/guide-skill-coupling.txt, the L8 pin); both then lint as usual. +# See the .py docstring for the check catalog (L1-L8). # Exit: 0 clean | 1 FAIL findings | 2 warnings only. ASCII + LF. set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/scripts/repo_lint.py b/scripts/repo_lint.py index cabf00c..64d56bc 100644 --- a/scripts/repo_lint.py +++ b/scripts/repo_lint.py @@ -35,9 +35,16 @@ mutates is the receipt file). Skipped when clientdocs/ is absent. NOTE: lint L-numbers are their own namespace, distinct from appendix-A lesson numbers (both have an L7). + L8 guide<->skill guide-to-skill coupling (skill-lag guard): the sha256 of + clientdocs/jenkins-kubernetes-guide.md is pinned in + clientdocs/guide-skill-coupling.txt. When the guide changes, + lint FAILS until the tenant AI skill (tenant-skill/references/ + kubernetes.md + troubleshooting.md) is reviewed for the same + update and the pin is re-recorded: bash scripts/repo-lint.sh + --record-guide-skill-coupling. Skipped when the guide is absent. Exit: 0 clean | 1 FAIL findings | 2 warnings only. ASCII + LF. -Usage: python3 scripts/repo_lint.py [--record-clientdocs-sweep] [repo-root] +Usage: python3 scripts/repo_lint.py [--record-clientdocs-sweep] [--record-guide-skill-coupling] [repo-root] (marker: repo-lint: allow-stale-tokens -- this file names them by necessity) """ import re, sys, pathlib, collections, hashlib @@ -58,16 +65,34 @@ # bash scripts/repo-lint.sh --record-clientdocs-sweep """ +COUPLING_FLAG = "--record-guide-skill-coupling" +COUPLING_CMD = "bash scripts/repo-lint.sh " + COUPLING_FLAG +COUPLING_HEADER = """\ +# clientdocs/guide-skill-coupling.txt -- guide<->skill coupling pin (L8). +# INTERNAL FILE: never shipped to a client. +# Pins the sha256 of the Jenkins + Kubernetes Implementation Guide. When the guide +# changes, repo lint FAILS until the tenant AI skill (tenant-skill/references/ +# kubernetes.md + troubleshooting.md) has been reviewed for the SAME update and this +# pin is re-recorded with: +# bash scripts/repo-lint.sh --record-guide-skill-coupling +# Rationale: guide edits repeatedly landed without mirroring into the skill +# (DOCFIX-135/136 skill-lag). This makes "sweep the skill" a hard gate, not a note. +""" + def main(): argv = [a for a in sys.argv[1:]] record = RECORD_FLAG in argv - argv = [a for a in argv if a != RECORD_FLAG] + coupling_record = COUPLING_FLAG in argv + argv = [a for a in argv if a not in (RECORD_FLAG, COUPLING_FLAG)] R = pathlib.Path(argv[0] if argv else ".").resolve() fails, warns = [], [] # ---- L7 helpers (clientdocs sweep-on-change, DOCFIX-104) ---- cdir = R / "clientdocs" receipt = cdir / "sweep-receipt.txt" + # ---- L8: guide<->skill coupling (skill-lag guard) ---- + guide = cdir / "jenkins-kubernetes-guide.md" + coupling = cdir / "guide-skill-coupling.txt" def sweep_auto(): # files the manifest MUST cover (existence-driven, no guesses) # recursive over clientdocs/: *.md anywhere (incl. tenant-skill/) and @@ -112,6 +137,14 @@ print(" [info] L7 record: dropped missing %s from receipt" % rp) receipt.write_text(RECEIPT_HEADER + "\n".join(body) + "\n") print(" [info] L7 recorded clientdocs sweep receipt (%d files)" % len(body)) + + if coupling_record: + if guide.is_file(): + coupling.write_text(COUPLING_HEADER + "%s %s\n" + % (sweep_sha(guide), guide.relative_to(R).as_posix())) + print(" [info] L8 recorded guide->skill coupling pin") + else: + print(" [info] L8 record: no jenkins-kubernetes-guide.md -- nothing to record") fails = [] # malformed lines were just rewritten; lint below re-reads def live_docs(): @@ -249,6 +282,24 @@ fails.append("L7 tenant-facing %s is not in the sweep receipt -- " "review clientdocs/, then: %s" % (rp, RECORD_CMD)) + # ---- L8 guide<->skill coupling (skill-lag guard) ---- + if guide.is_file(): + if not coupling.exists(): + fails.append("L8 clientdocs/guide-skill-coupling.txt missing -- review the " + "tenant skill vs the guide, then: %s" % COUPLING_CMD) + else: + pinned = None + for ln in coupling.read_text(errors="replace").splitlines(): + ln = ln.strip() + if ln and not ln.startswith("#"): + pinned = ln.split(None, 1)[0] + break + if pinned != sweep_sha(guide): + fails.append("L8 jenkins-kubernetes-guide.md changed since the last " + "guide->skill review -- review clientdocs/tenant-skill/" + "references/kubernetes.md + troubleshooting.md for the SAME " + "update, then re-record: %s" % COUPLING_CMD) + for w in warns: print(" [WARN] %s" % w) for f in fails: diff --git a/tests/repo-lint/run-tests.sh b/tests/repo-lint/run-tests.sh index 2e08229..05545ac 100644 --- a/tests/repo-lint/run-tests.sh +++ b/tests/repo-lint/run-tests.sh @@ -56,6 +56,15 @@ echo "$d" } rec() { python3 "$REPO/scripts/repo_lint.py" --record-clientdocs-sweep "$1" >/dev/null 2>&1; } +mkguide() { # mkcd + a Jenkins guide + skill refs (for the L8 guide<->skill coupling tests) + local d; d=$(mkcd "$1") + printf '# guide v1\n' > "$d/clientdocs/jenkins-kubernetes-guide.md" + mkdir -p "$d/clientdocs/tenant-skill/references" + printf '# k8s\n' > "$d/clientdocs/tenant-skill/references/kubernetes.md" + printf '# trbl\n' > "$d/clientdocs/tenant-skill/references/troubleshooting.md" + echo "$d" +} +reccpl() { python3 "$REPO/scripts/repo_lint.py" --record-guide-skill-coupling "$1" >/dev/null 2>&1; } D=$(mkcd l7a); run 1 'L7 clientdocs/ exists without sweep-receipt' \ "T10 L7 missing receipt FAILS" "$D" @@ -102,5 +111,25 @@ printf '# em\xe2\x80\x94dash noise\n' > "$D/.claude/worktrees/agent-x/docs/design-decisions.md" run 0 'PASS: repo lint' "T23 .claude/ contents excluded from lint" "$D" +# ---- L8 guide<->skill coupling (skill-lag guard) ---- +# guide present + L7 recorded, but no coupling pin -> L8 FAILS +D=$(mkguide l8a); rec "$D" + run 1 'L8 clientdocs/guide-skill-coupling.txt missing' \ + "T24 L8 missing coupling pin FAILS" "$D" +# pin recorded -> PASSES +D=$(mkguide l8b); rec "$D"; reccpl "$D" + run 0 'PASS: repo lint' "T25 L8 recorded coupling PASSES" "$D" +# guide edited, L7 re-recorded (so L7 is clean) but skill NOT re-reviewed -> L8 FAILS alone +D=$(mkguide l8c); rec "$D"; reccpl "$D" +printf '# guide v2 (new topic)\n' >> "$D/clientdocs/jenkins-kubernetes-guide.md"; rec "$D" + run 1 'L8 jenkins-kubernetes-guide.md changed since the last' \ + "T26 L8 guide drift FAILS when skill not re-reviewed" "$D" +# after re-recording the coupling (the review ack) -> PASSES again +D=$(mkguide l8d); rec "$D"; reccpl "$D" +printf '# guide v2\n' >> "$D/clientdocs/jenkins-kubernetes-guide.md"; rec "$D"; reccpl "$D" + run 0 'PASS: repo lint' "T27 L8 re-record after review PASSES" "$D" +# no guide in the fixture -> L8 is skipped entirely (clean repo still passes) +D=$(mkcd l8e); rec "$D"; run 0 'PASS: repo lint' "T28 L8 skipped when no guide present" "$D" + echo; echo "RESULT: PASS=$PASS FAIL=$FAIL" [[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1