Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ jobs:
# merge-base. ~50 commits, so the full history costs nothing here.
fetch-depth: 0

# The hook, the two checkers and their three suites are the only executables here,
# The hook, the two checkers, the Python metrics report and their four suites are
# the only executables here,
# so lint plus those suites are the only mechanical gates we have. Everything else
# here is a prompt, and prompts have no typechecker (see README, "Contributing").

Expand Down Expand Up @@ -69,6 +70,12 @@ jobs:
--shell=sh scripts/check-version-bump.sh
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--shell=sh scripts/check-version-bump.test.sh
# The metrics report's suite (vision step 2b). The report itself is Python, so
# only its POSIX-sh suite is linted here; the suite runs the report under the
# runner's python3. Same `[ c ] && pass || fail` lines as the other suites,
# hence SC2015.
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--shell=sh --exclude=SC2015 scripts/ledger-metrics.test.sh

# Two runs, because the hook has to be correct under both shells and the runner's
# /bin/sh is dash while a contributor's may be bash. HOOK_SH selects the shell the
Expand All @@ -89,11 +96,12 @@ jobs:
# Each rule was prose first and each was violated anyway — a floating action ref
# shipped in this very workflow, a duplicate hooks manifest key stopped the plugin
# loading in 0.2.1, and the plugin shipped changes without a bump twice.
- name: Invariant checks (pinning, manifest, prompt conformance) + both checker suites
- name: Invariant checks (pinning, manifest, prompt conformance) + checker and report suites
run: |
sh scripts/check-invariants.test.sh
sh scripts/check-invariants.sh
sh scripts/check-version-bump.test.sh
sh scripts/ledger-metrics.test.sh

# Invariant 12 itself needs a base to diff against, so it runs only on pull
# requests — a push to main has no PR base, and diffing the push range would just
Expand Down
17 changes: 11 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ scripts/check-invariants.sh # invariants 5, 6 + prompt conformance (rung 2
scripts/check-invariants.test.sh # its regression suite — reject/accept pairs
scripts/check-version-bump.sh # invariant 12, mechanically — PR-only (rung 2)
scripts/check-version-bump.test.sh # its regression suite — policy/operational/accept
scripts/ledger-metrics.py # read-only report over the ledger + git (vision step 2b)
scripts/ledger-metrics.test.sh # its regression suite — fixture repos, expected text
plugins/dev-workflow/
.claude-plugin/plugin.json # metadata only — no component keys (invariant 6)
CHANGELOG.md # every manifest version, newest first
Expand All @@ -62,8 +64,9 @@ source-files/ # the extraction seed this repo was built from
**Boundaries.** `skills/`, `commands/`, `agents/` and `hooks/hooks.json` are loaded by convention
from their paths. The executable artifacts are the hook and its test, plus the two
repo-local CI checkers and their tests (`scripts/check-invariants.{sh,test.sh}` and
`scripts/check-version-bump.{sh,test.sh}`) — the hook ships in the plugin, the checkers
do not; everything else is text read by a model. `examples/` is reference material,
`scripts/check-version-bump.{sh,test.sh}`), plus the read-only Python report
`scripts/ledger-metrics.py` and its suite `scripts/ledger-metrics.test.sh` — the hook ships
in the plugin, the checkers and the report do not; everything else is text read by a model. `examples/` is reference material,
outside the loaded surface: never scaffolded or copied into a user's project, though it
does ship inside the plugin package.

Expand Down Expand Up @@ -253,9 +256,9 @@ Every command below was run in this session and observed to exit 0.

| Role | Command |
|---|---|
| quality (the whole battery — what CI runs) | `shellcheck --shell=sh plugins/dev-workflow/hooks/codex-gate.sh && shellcheck --shell=sh --exclude=SC2015 plugins/dev-workflow/hooks/codex-gate.test.sh && shellcheck --shell=sh scripts/check-invariants.sh && shellcheck --shell=sh --exclude=SC2015 scripts/check-invariants.test.sh && shellcheck --shell=sh scripts/check-version-bump.sh && shellcheck --shell=sh scripts/check-version-bump.test.sh && HOOK_SH=sh sh plugins/dev-workflow/hooks/codex-gate.test.sh && HOOK_SH=dash dash plugins/dev-workflow/hooks/codex-gate.test.sh && sh scripts/check-invariants.test.sh && sh scripts/check-invariants.sh && sh scripts/check-version-bump.test.sh && sh scripts/check-version-bump.sh main && claude plugin validate . --strict` |
| typecheck | n/a — no typed sources (shell + markdown) |
| lint | `shellcheck --shell=sh plugins/dev-workflow/hooks/codex-gate.sh && shellcheck --shell=sh --exclude=SC2015 plugins/dev-workflow/hooks/codex-gate.test.sh && shellcheck --shell=sh scripts/check-invariants.sh && shellcheck --shell=sh --exclude=SC2015 scripts/check-invariants.test.sh && shellcheck --shell=sh scripts/check-version-bump.sh && shellcheck --shell=sh scripts/check-version-bump.test.sh` |
| quality (the whole battery — what CI runs) | `shellcheck --shell=sh plugins/dev-workflow/hooks/codex-gate.sh && shellcheck --shell=sh --exclude=SC2015 plugins/dev-workflow/hooks/codex-gate.test.sh && shellcheck --shell=sh scripts/check-invariants.sh && shellcheck --shell=sh --exclude=SC2015 scripts/check-invariants.test.sh && shellcheck --shell=sh scripts/check-version-bump.sh && shellcheck --shell=sh scripts/check-version-bump.test.sh && shellcheck --shell=sh --exclude=SC2015 scripts/ledger-metrics.test.sh && HOOK_SH=sh sh plugins/dev-workflow/hooks/codex-gate.test.sh && HOOK_SH=dash dash plugins/dev-workflow/hooks/codex-gate.test.sh && sh scripts/check-invariants.test.sh && sh scripts/check-invariants.sh && sh scripts/check-version-bump.test.sh && sh scripts/check-version-bump.sh main && sh scripts/ledger-metrics.test.sh && claude plugin validate . --strict` |
| typecheck | n/a — no typed sources (shell, one untyped Python report, markdown) |
| lint | `shellcheck --shell=sh plugins/dev-workflow/hooks/codex-gate.sh && shellcheck --shell=sh --exclude=SC2015 plugins/dev-workflow/hooks/codex-gate.test.sh && shellcheck --shell=sh scripts/check-invariants.sh && shellcheck --shell=sh --exclude=SC2015 scripts/check-invariants.test.sh && shellcheck --shell=sh scripts/check-version-bump.sh && shellcheck --shell=sh scripts/check-version-bump.test.sh && shellcheck --shell=sh --exclude=SC2015 scripts/ledger-metrics.test.sh` |
| test | `HOOK_SH=sh sh plugins/dev-workflow/hooks/codex-gate.test.sh && HOOK_SH=dash dash plugins/dev-workflow/hooks/codex-gate.test.sh` — two runs; `HOOK_SH` selects the shell the HOOK runs under, and without it a dash invocation only exercises the harness |
| invariant checks (5 pinning, 6 manifest, prompt conformance) | `sh scripts/check-invariants.test.sh && sh scripts/check-invariants.sh` |
| invariant check (12 version bump) | `sh scripts/check-version-bump.test.sh && sh scripts/check-version-bump.sh main` |
Expand All @@ -268,7 +271,9 @@ twice, once with the hook under `sh` and once under `dash`, because the hook has
correct under both and Ubuntu's `/bin/sh` IS dash. Bump the first two deliberately, per
invariant 5; `dash` is addressed by name because it is the system shell, not a pinned
tool. Without it the second run cannot start, and dropping that run is what let a
`dash`-only defect ship once already.
`dash`-only defect ship once already. The metrics report's suite also needs
**`python3`** 3.8 or later, addressed by name for the same reason as `dash`: it is the
system interpreter, not a pinned tool.

**The `--exclude=SC2015` on the test file** is a single-code exclusion, not a blanket
disable: every other shellcheck rule still applies to that file. Its hits are all
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1358,9 +1358,9 @@ like the rest of §5; the detection is a reader comparing the pass against the s
**Every cycle records one provenance line in its closing commit body** — default floor or
not, so an absent line is never ambiguous between "the default applied" and "someone forgot".
**One line per cycle**, so a change running five cycles records five. There is no informal
variant; anything quoting this form elsewhere quotes an instance of it, because the deferred
metrics work is intended to parse it — that consumer does not exist yet, and the form is pinned
now so that it can.
variant; anything quoting this form elsewhere quotes an instance of it, because tooling parses
it — in this repository `scripts/ledger-metrics.py` (dark-factory vision step 2b) — and the
form is pinned so that it can.

<CYCLE-FIELD>; floor <N> per <STORY-SET>; hook reminder threshold <KNOB>

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,15 @@ honest gap ([reasoning](docs/coding-workflow.md#adapting-it-to-another-project))
## Contributing

CI ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs four checks on every
PR and push to main: `shellcheck --shell=sh` over all three executables and their test
files, the hook's test suite,
PR and push to main: `shellcheck --shell=sh` over the three shell executables and every
shell test file, the hook's test suite,
[`scripts/check-invariants.sh`](scripts/check-invariants.sh) (invariants 5 and 6, plus three prompt-conformance checks) plus
both checkers' regression suites, and `claude plugin validate . --strict`.
both checkers' regression suites, the metrics report's suite, and
`claude plugin validate . --strict`.

`python3 scripts/ledger-metrics.py` prints a read-only report over the hardening ledger
and the review-cycle records in commit bodies: which fingerprints recur, how rungs were
followed, and each cycle's recorded curve beside the `fic2` baseline. It writes nothing.

A fifth check runs **on pull requests only**:
[`scripts/check-version-bump.sh`](scripts/check-version-bump.sh) (invariant 12), which
Expand Down
1 change: 1 addition & 0 deletions docs/hardening-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ escape `\|`, one line), `source` (gate-a|gate-b|bot|manual),
| 2026-09-02 | verification-masks-failure | fifth occurrence: PR #26 (CodeRabbit) — Plan C task 25's completion assert tested only that the provisional wording was ABSENT (`grep -c … -eq 0`). A replacement that deleted the provisional passage and wrote no closed record at all would have passed it, which is precisely the outcome the task exists to prevent. The task did run correctly this cycle, so the mask never fired; it was found by reading, not by failing | bot | major | 4 test | The assert gained a positive arm beside the negative one: the closed curves must be PRESENT, with a cardinality floor (`grep -cE '^Findings( +[0-9]+,?)+' … -ge 3`). PRIOR ROWS under this fingerprint all share one shape — a check whose only assertion is that something is gone. WHAT GENERALIZES: an absence assert is half a check whenever the edit it guards is a replacement rather than a deletion, and the missing half is always the same one. This row's remedy is specific to task 25; the general form belongs to the loop-rule consolidation story, which owns the plan-assert conventions |
| 2026-09-25 | missing-input-validation | first row of this base class here: PR #27 (Greptile P1, thread 4091660211) — `/dev-workflow:claude-init` filled an unconstrained project name into the create command's shell here-document; a name with a line break could put the fixed delimiter on its own line, end the document early and run the rest of the name and the template as shell. Confirmed as transport under sh and dash with a harmless marker, not as an end-to-end command run | bot | blocker | 1 prose | claude-init.md *Writing* step 2 now states the rule (one line, no Unicode `Cc` character, every name source, ask on failure, nothing sanitized) and gives a Python check for a directory-derived name; spec §2 carries it. AGENTS.md Don'ts gains the class-level rule: data spliced into shell source a prompt tells the agent to run must be bounded, with the rule, the check and the failure path stated. Does NOT guard: the rule is agent-followed — the create script does not validate the name, and a user-given name is checked by reading. NO DETERMINISTIC RUNG EXISTS: nothing mechanical can tell which prompt text becomes shell source; it is a reading check |
| 2026-09-30 | docs-drift | eighth occurrence: PR #32 (Greptile P2) — the sparring-skill spec said "The eight walkthrough scenarios … the last two were added by pass 1" above a list numbering twelve; the second Gate-A repair round added 9–12 and left the count sentence. Gate-A spec pass 3 had already found it and collected it as a NIT, so detection held and only the collect-only triage let it ship. Fixed in the same PR (the sentence now says twelve and names which round added which). | bot | nit | 1 prose | No new text: the existing guard already covers it — CLAUDE.md §5 Gate-B lens, "Name what this diff changes the size, value or position of … and grep for where each is described elsewhere", which a Gate-A repair that grows an enumerated list should apply to its own count sentence. NO DETERMINISTIC RUNG EXISTS for this shape: the rung-2 count lint (2026-07-26 row) matches only the "all N checklist items" spelling, and docs/superpowers/ is excluded from it on purpose as dated records. Recorded so the recurrence count stays true; escalate if a count-vs-list drift ships from a shipped prompt rather than a historical artifact. PRIOR ROW: 2026-09-02 docs-drift (1 prose). |
| 2026-10-01 | docs-drift | ninth occurrence: PR #33 (Greptile) — the passive-metrics spec still described a 10000-pass limit that the implementation had removed under plan ruling 1, and the executed plan still expected `30 passed` after Gate B added suite cases. Both were statements about this same change, left behind by later steps of it. Fixed in the same PR: the spec now states the implemented rule, marked as updated after implementation, and the plan carries a dated note that its counts describe the suite as approved. | bot | minor | 1 prose | No new text: CLAUDE.md §5 Gate B already says a fix that changes specified behaviour updates the spec in the same commit, and that rule covers this case. What slipped was applying it to a ruling made in the plan rather than a Gate-B fix. NO DETERMINISTIC RUNG: nothing can tell that a ruling contradicts spec prose. PRIOR ROW: 2026-09-30 docs-drift (1 prose). |
Loading