Repository navigation
verify-no-work-records: catch mixed-case PLAN-/DESIGN-/REPORT-/STATUS- files at any depth #89
Description
Activity
- changed the title
[-]verify-no-work-records: treat decisions/** as documentation and replace the root-only name heuristic with an explicit allowlist[/-][+]verify-no-work-records: catch mixed-case PLAN-/DESIGN-/REPORT-/STATUS- files at any depth[/+]on Sep 23, 2026 Numbered
decisions/records already pass the guard. What is still missing is catching mixed-case work-record names such asPlan-…ordesign-…and files below the repository root.Investigation
What I found
Reproduced the gap. I extracted the inline script from
.github/workflows/verify-no-work-records.ymland ran it against a fixture git repo. Against the issue's own fixture set, today's guard:File Today Wanted PLAN-foo.md,REPORT-weekly.md(root)passes fail Plan-bar.md(root, mixed case)passes fail notes/DESIGN-bar.md,Notes/Plan-something.md,Source/Report-weekly.mdpasses fail DECISIONS.md(root index)fails pass decisions/D-0001-example.md,Documentation/decisions/0003-*.mdpasses pass decisions/HANDOVER.mdfails fail Cause, confirmed by reading
verify-no-work-records.yml:52and:61: rule 2's regex^[A-Z][A-Z0-9_.-]*[A-Z0-9]\.md$is root-anchored and rejects any lowercase, soPLAN-foo.md(lowercasefoo) never matches; rule 3 lists onlyHANDOVER|PROMPT-|NEXT-SESSION|SESSION-PROMPT|SESSION[-_]HANDOVER, so noPLAN/DESIGN/REPORT/STATUSclass exists at any depth. Mixed-caseHandover.mdalso passes today.Measured the blast radius before proposing a regex. This reusable workflow is bootstrapped into ~50 Cratis repos, most of them at
@main(Chronicle, Strategy, …), a few pinned (Chronicle.Dapr, Chronicle.Wolverine). I pulled the full file tree of all 55 non-archived org repos (73k paths) and replayed candidate rules over them:- Fully case-insensitive prefixes (
design-…as the comment asks) → 13 new failures today:Chronicle*/Documentation/client-snippets/.../design-for-async.md(4 repos),Strategy/governance/plan-coverage-audit.md,Strategy/evidence/design-partner-agreement-checklist.md, plus 6 inAI(no wrapper installed, so harmless). - Case-insensitive + documentation-directory allowlist → 2 new failures, both in
Strategy, which tracks@main. - All-caps or Titlecase prefixes (
PLAN-/Plan-,DESIGN-/Design-, …) → 0 new failures org-wide, while still catching every case in the issue's fixture and the mutation proof.
Also: a naive case-insensitive substring
handoverrule would newly fail a real ADR,Ensemble/Documentation/.../0006-planning-roadmap-handover-authority.md— so the session class must stay anchored, not substring, for the new casings.Prototype verified. I built the proposed rule and ran it: the clean fixture prints
No AI work records tracked (12 markdown files checked).exit 0; addingNotes/PLAN-something.mdfails naming that path; the dirty fixture flags all six missed files anddecisions/HANDOVER.md, whileDECISIONS.md,decisions/D-0001-example.mdandDocumentation/decisions/0003-*.mdpass.QUESTION is not needed for the main body of work, but one sub-decision below needs a yes/no — I've stated my recommendation and a default so the work is not blocked.
SUGGESTED-TIER: powerful
Plan — #89
Settled (do not redo)
- The gap is reproduced, with the exact cause at
verify-no-work-records.yml:52(root-anchored, all-caps-only) and:61(no PLAN/DESIGN/REPORT/STATUS class). No further diagnosis needed. - Keep the logic inline in the reusable workflow.
actions/checkoutin aworkflow_calljob fetches the caller's repo, so.github/scripts/*.shfrom this repo is not on the runner — the repo already documents this deliberately (cleanup-pr-artifacts.yml:47). Do not extract to a script file or add a second checkout. - The repo's test pattern is: a Python test that extracts the inline
run:block from the YAML and exercises it (.github/scripts/tests/cleanup-pr-artifacts.test.py), wired into averify-*.ymlgate withpaths:filters. Follow it. - The maintainer's comment narrows the issue body: numbered
decisions/records already pass; the work is detection of mixed-case names at any depth. The body's "re-syncAGENTS.md" bullet is stale —AGENTS.mdis now a pointer into the managed.cratis/ai/corpus, which must not be hand-patched from this repo. Drop that bullet. - The verified rule shape (prototype run against both fixtures and all 55 org repos) is below.
Changes
1.
.github/workflows/verify-no-work-records.yml— detectionAdd a documentation-directory allowlist used by the shape rule only, add the shape rule, and extend the session rule with anchored Titlecase forms (keeping the existing uppercase substring forms so nothing currently caught is lost):
docs='^(decisions|Decisions|Documentation|documentation|docs|Docs|Knowledge)/' # work-record shape prefixes at any depth, all-caps or Titlecase shape='(^|/)(PLAN|Plan|DESIGN|Design|REPORT|Report|STATUS|Status)([-_][^/]*)?\.md$' # ... skip .claude/ .github/ .pi/ .agents/ .cratis/ .ai-work/ (same case list as rule 3), # ... then `grep -qE "$docs" && continue`, else violation "work-record-style document: $f" # session class: existing uppercase substrings PLUS anchored Titlecase names session='...existing...|(^|/)(Handover|Prompt|Next-Session|Session)([-_][^/]*)?\.md$'
Constraints that matter and were verified:
- The
docsallowlist exempts the shape rule only, never the session rule —decisions/HANDOVER.mdmust still fail (issue acceptance). - The session class stays anchored for the new casings; a case-insensitive substring would flag
Ensemble/Documentation/.../0006-planning-roadmap-handover-authority.md. - Prefixes require a
-/_separator or end-of-name, soReporting.md,Designs.md,Planning.mdare untouched. ExactPLAN.md-style names at depth: zero in the org today, safe to include.
2. Same file — root allowlist
Add
DECISIONSto the rule-2allowlist (a root decisions index is documentation; the issue's acceptance requires it to pass). Verified: no repo in the org has a rootDECISIONS.mdtoday, so this hides nothing currently caught.3. New input
extra-allowed-paths(optional, recommended)Comma-separated directory prefixes appended to
docs, so a repo with a non-standard documentation folder does not need a code change here. Today'sextra-allowedonly affects root file names — that limitation is what the issue calls "a growing root allowlist"; two repos already carryextra-allowedvalues (Chronicle.Dapr: LIFECYCLE,RELEASE,THIRD-PARTY-NOTICES,VERSIONS,Chronicle.Wolverine: VERSIONS). Adding an input is backward compatible; the bootstrapped wrapper needs no change.4.
.github/scripts/tests/verify-no-work-records.test.pyModel on
cleanup-pr-artifacts.test.py:SHELL = textwrap.dedent(SOURCE.split(" run: |\n", 1)[1] ...), then run it withbashinsidetempfilegit repos (git init, write files,git add, commit — the guard readsgit ls-files, so files must be tracked). Cases, all confirmed against the prototype:- clean:
README.md,DECISIONS.md,decisions/D-0001-example.md,decisions/index.md,Documentation/decisions/0003-kernel-boundary.md,Documentation/client-snippets/design-for-async.md,Source/Design/overview.md,Source/Reporting.md,Documentation/adrs/0006-planning-roadmap-handover-authority.md→ exit 0 - dirty, one assertion per path with the path named in the output:
PLAN-foo.md,Plan-bar.md,REPORT-weekly.md,notes/DESIGN-bar.md,Notes/Plan-something.md,Source/Report-weekly.md,Source/Status-board.md,decisions/HANDOVER.md,docs/Handover.md,.ai-work/anything.md - regression:
.github/PROMPT-x.mdand.cratis/…stay skipped - mutation proof from the issue:
Notes/PLAN-something.mdadded to the clean fixture makes it fail naming that file
5.
.github/workflows/verify-work-record-guard.ymlNew gate, copied from
verify-pr-artifact-cleanup.yml(pinnedactions/checkoutSHA,persist-credentials: false,permissions: contents: read,timeout-minutes: 5),paths:coveringverify-no-work-records.yml, the new test, itself, andREADME.md.6.
README.mdAdd
### verify-no-work-records.ymlunder Workflows in this repository: the three rule classes, the root-name allowlist, the documentation-directory allowlist, and both inputs (runs-on,extra-allowed,extra-allowed-paths).Verification
python3 .github/scripts/tests/verify-no-work-records.test.py(offline, no GitHub contact).actionlinton both changed workflows (README line 148 shows the repo's usage).- Re-run the org-wide replay before merge (fetch
repos/Cratis/<repo>/git/trees/HEAD?recursive=1for non-archived repos, apply the final regexes): the expected result is 0 new failures. This matters because ~50 repos consume this workflow and most track@main, so a false positive turns theirmainred on merge.
Commits / PR
One branch, separate commits (workflow rule change → tests → gate workflow → README). Label
no-release— this repo ships no package and the change is CI-only. Consider landing #90 (exit 2 /--self-test/ print counts when green) on the same branch: it touches the same file and the same new test harness, andpull-requests.mdsays related small changes are one PR. If they are done separately, land #89 first.Needs a human decision (one, with a safe default)
Should all-lowercase
design-…/plan-…files be flagged too? The issue comment namesdesign-…explicitly, but the data says it is not free:- Recommended (my default, already prototyped): capitalized prefixes only (
PLAN-,Plan-,DESIGN-,Design-, …). Catches everything in the issue's fixture and the mutation proof, 0 new failures across all 55 org repos, and matches the original author's stated intent atverify-no-work-records.yml:59("uppercase convention only, to spare real docs"). Misses a genuinely lowercasenotes/design-bar.md. - Full case-insensitivity + the documentation allowlist: turns
Strategy'smainred today overgovernance/plan-coverage-audit.mdandevidence/design-partner-agreement-checklist.md(that repo tracks@main), and carries standing false-positive risk for any futuredesign-*.mdoutside a documentation folder. Viable only if those two files are renamed, orStrategy's wrapper getsextra-allowed-paths: governance/,evidence/, before this merges.
Unless told otherwise, implement the capitalized variant and note the lowercase option in the PR description so it can be turned on later by widening one regex.
Suggested tier for implementation:
powerful
Posted by Direct (AI) - an autonomous agent, not a person. Review accordingly.
- Fully case-insensitive prefixes (
Completed in #150. The inline check is now a tested script with exit code 2 for could-not-run, counts, mixed-case matching and per-violation annotations, pinned by commit and checksum. Follow-ups: none.
Summary
Amend
verify-no-work-records.ymland the inlined rule text so that tracked decision records are documentation, not work records, and replace the root-only name heuristic with an explicit documentation allowlist that also catches the mixed-casePLAN-*/DESIGN-*/REPORT-*files the current regexes miss.Current behaviour
.github/workflows/verify-no-work-records.yml(one input,extra-allowed, lines 12-16) applies three rules:.ai-work/(line 39);.mdfiles matched by^[A-Z][A-Z0-9_.-]*[A-Z0-9]\.md$(line 52) unless in the root allowlist at line 43 (README|LICENSE|AGENTS|CLAUDE|GEMINI|CODE_OF_CONDUCT|CONTRIBUTING|SECURITY|CHANGELOG|CREDITS|RESOURCES|BRAND|MESSAGING|PAGES|SITE|PRIVACY_POLICY|ROADMAP|START-HERE|CHRONICLE|COMPATIBILITY|NOTICE|SUPPORT|GOVERNANCE|VERSION, plusextra-allowedat line 47);HANDOVER,PROMPT-*,NEXT-SESSION,SESSION-PROMPT,SESSION[-_]HANDOVERnames anywhere (line 61), skipping.ai/* .claude/* .github/* .pi/* .agents/* .ai-work/*(line 58).Consequences:
DECISIONS.mdfails rule 2;decisions/HANDOVER.mdfails rule 3; nothing saysdecisions/**(orDocumentation/decisions/**,Knowledge/Decisions/**) is documentation, so a repository adopting a decisions folder has to guess what the gate will accept. The rule text inlined inAGENTS.md:11("Knowledge that must outlive the session belongs in the repository's documentation structure through normal review") names no location; the canonical copy in Cratis/AI mentions ADRs, this copy does not.PLAN-workbench-product.mdorDESIGN-workbench-*.mdare not SCREAMING-CASE (they contain lowercase after the prefix) and match none of rule 3's names, so a planning document with aPLAN-orDESIGN-prefix passes the gate at the root and anywhere below it.extra-allowedin every wrapper.Desired behaviour
decisions/**,Documentation/**,docs/**,Knowledge/Decisions/**(plus the existing root names) are never work records regardless of case; the rule text gains "a decision log is not a work record; decisions live indecisions/(or the repository's documented decisions folder)".PLAN-,DESIGN-,REPORT-,STATUS-,HANDOVER,PROMPT-,NEXT-SESSION,SESSION-) and applies below the root as well, excluding the documentation allowlist and the existing skip list.AGENTS.mdre-synced from the canonical rule so the ADR/decision wording is present.Acceptance
Done when:
decisions/D-0001-example.md,Documentation/decisions/0003-kernel-boundary.mdand a rootDECISIONS.mdindex passes; the same fixture withPLAN-foo.mdat the root,docs/../notes/DESIGN-bar.mdoutside the allowlist, ordecisions/HANDOVER.mdfails, each with the offending path printed.README.mddocuments the allowlist and the wrapper inputs.Verify by:
Mutation proof: add
Notes/PLAN-something.mdto the clean fixture; the run fails naming it.Out of scope
Exit codes and
--self-test(separate issue in this repository); the shared rule text in Cratis/AI (its own issue); recovering any repository's untracked decisions (their own issues).Dependencies
Source: AI-Learning F-88 (gate half), F-99. Depends on: D-0001 (where decisions live). Related: the
verify-decisionsreusable workflow issue and the exit-2/self-test issue in this repository; the rule amendment issue in Cratis/AI.Status
State: Done
Scope: The work-records guard is a pinned, checksummed and tested script with exit code 2 for could-not-run, a --self-test, counts and mixed-case matching.
Delivered: - #150 (merged)
Remaining: Nothing.
Follow-ups: None.