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
58 changes: 45 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
# shellcheck image and claude CLI avoid. The runner is pinned to an OS release
# rather than `ubuntu-latest` — GitHub still refreshes that image's contents
# weekly, so this bounds the drift, it does not eliminate it. Full image
# reproducibility would need a container, which this four-check battery
# reproducibility would need a container, which this check battery
# doesn't warrant.
runs-on: ubuntu-24.04
steps:
Expand All @@ -32,15 +32,22 @@ jobs:
# later step — or anything a step runs — can read it. Nothing here needs
# authenticated git after the clone, so don't leave it lying around.
persist-credentials: false
# The version-bump check diffs against the merge-base with the PR's base
# branch, and the default depth-1 clone has neither the base branch nor a
# merge-base. ~50 commits, so the full history costs nothing here.
fetch-depth: 0

# The hook, the invariant checker and their two suites are the only executables here, so lint plus their test
# suite are the only mechanical gates we have. Everything else here is a prompt,
# and prompts have no typechecker (see README, "Contributing").
# The hook, the two checkers and their three 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").

# Enforces the POSIX-sh invariant mechanically: the hook runs under whatever
# /bin/sh a user has, so a bash-ism is a portability bug, not a style opinion.
# POSIX sh, for two different reasons that must not be blurred: the HOOK is bound
# by invariant 4, because it runs under whatever /bin/sh a user has, so a bash-ism
# there is a portability bug rather than a style opinion; the two repo-local
# checkers are bound by this workflow invoking them with `sh`, which is their own
# requirement and not invariant 4's reach.
# Pinned to an exact image tag — a moving linter makes the gate irreproducible.
- name: Lint hook scripts (POSIX sh)
- name: Lint shell scripts (POSIX sh)
run: |
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--shell=sh plugins/dev-workflow/hooks/codex-gate.sh
Expand All @@ -50,23 +57,48 @@ jobs:
# every other check still applies to this file.
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--shell=sh --exclude=SC2015 plugins/dev-workflow/hooks/codex-gate.test.sh
# The invariant checker is the repo's second executable artifact and is a
# CI gate itself, so it gets the same POSIX lint as the hook.
# The two repo-local checkers are CI gates themselves, so they get the same
# POSIX lint as the hook.
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--shell=sh scripts/check-invariants.sh
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--shell=sh --exclude=SC2015 scripts/check-invariants.test.sh
# No exclusion on this pair: unlike the two suites above, it has no
# `[ c ] && pass || fail` lines, so every rule applies as-is.
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:v0.11.0 \
--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

- name: Hook state-machine tests
run: sh plugins/dev-workflow/hooks/codex-gate.test.sh

# Invariants 5 and 6, mechanically. Both were prose first and both were
# violated anyway — a floating action ref shipped in this very workflow, and a
# duplicate hooks manifest key stopped the plugin loading in 0.2.1.
- name: Invariant checks (pinning, manifest)
# Invariants 5 and 6 mechanically, plus BOTH checkers' regression suites. The
# invariant-12 checker itself is not here — it needs a PR base and runs in the
# step below, so naming this step "version bump" would show a green version-bump
# label on a push that was never version-bump checked.
# 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) + both checker suites
run: |
sh scripts/check-invariants.test.sh
sh scripts/check-invariants.sh
sh scripts/check-version-bump.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
# re-check what the PR run already gated. Consequence, stated rather than implied:
# a commit pushed straight to main bypasses this check entirely.
#
# base_ref goes through env, never into the run script via ${{ }}: git ref names
# may contain shell metacharacters, and direct interpolation would splice a branch
# name in as code rather than pass it as data.
- name: Version bump required for plugin changes (invariant 12)
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.base_ref }}
run: sh scripts/check-version-bump.sh "origin/$BASE_REF"

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand Down
64 changes: 53 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ under `.mcp/`) is deliberately absent, not overlooked.

```
README.md # what the kit is, setup, daily use, contributing
MANIFEST.md # what each shipped file is and where it comes from
MANIFEST.md # inventory of source-files/, the frozen extraction seed
AGENTS.md # this file — the invariants both gates check
CLAUDE.md # discipline rules + the two review gates
todos.md # backlog; `pending` ledger rows point here by ref
Expand All @@ -36,13 +36,16 @@ todos.md # backlog; `pending` ledger rows point here by
.github/workflows/ci.yml # lint + hook tests + invariant checks + validate
scripts/check-invariants.sh # invariants 5 and 6, mechanically (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
plugins/dev-workflow/
.claude-plugin/plugin.json # metadata only — no component keys (invariant 6)
CHANGELOG.md # every manifest version, newest first
skills/{intake,harden-finding}/SKILL.md
agents/finding-triage.md # read-only PR-comment checker (convention-loaded)
commands/{workflow-init,process-pr-review}.md
hooks/{hooks.json,codex-gate.sh,codex-gate.test.sh}
examples/ # read, don't install — one stack's answers
examples/ # ships, but never scaffolded — one stack's answers
docs/
architecture.md # layout + the two non-obvious design decisions
coding-workflow.md # the methodology this plugin encodes
Expand All @@ -51,14 +54,17 @@ docs/
hardening-log.md # append-only findings ledger (union-merged)
hardening-taxonomy.md # this project's fingerprint classes
pr-review-bots.md # the Wait-for routing list + where each bot's findings appear
superpowers/{specs,plans,stories}/ # the approved artifacts behind past changes
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
`scripts/check-invariants.sh` and its test (the hook ships in the plugin; the checker
is repo-local CI); everything else is text
read by a model. `examples/` is reference material, outside the loaded surface.
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,
outside the loaded surface: never scaffolded or copied into a user's project, though it
does ship inside the plugin package.

**Dependency direction.** The plugin depends on superpowers (skills it hands off to)
and on a Codex MCP server exposing both `exec` and `review` (the gates key on those
Expand Down Expand Up @@ -109,8 +115,34 @@ reader can judge whether it still holds.
`commands/`, `agents/` and `hooks/hooks.json` load automatically; a manifest key for them is
redundant at best and fatal for hooks (duplicate-hooks error → the plugin does not
load at all; fixed in 0.2.1). Manifest keys only for files outside convention paths.
7. **`examples/` is read-only reference.** Never installed, never copied by a command,
never presented as a default — it encodes one stack's answers and will not transfer.
7. **`examples/` is read-only reference.** Never scaffolded or copied into a user's
project, never presented as a default — it encodes one stack's answers and will not
transfer. It *does* ship inside the plugin package (the cache copies the plugin
directory wholesale), which is why invariant 12 covers it: shipped-but-not-scaffolded
is not the same claim as not-shipped, and reading it as the latter once made
`examples/` look out of scope for the version-bump rule.
12. **A plugin change requires a version bump.** A pull request that changes any path
under a `plugins/<name>/` directory **that still exists at HEAD** — `examples/`
included, and no exemptions among the paths inside such a directory — must also
change that plugin manifest's `version`, or CI fails. (Deleting a whole plugin
directory is the one shape outside the rule, since nothing of it ships afterwards;
deleting only its manifest while the directory survives fails closed.) The checker is
`scripts/check-version-bump.sh`, run **on pull requests only**, with
`scripts/check-version-bump.test.sh` as its suite. An installed copy lives under a
version-keyed cache path, so an un-bumped change never reaches it: the machine keeps
running the old code with no signal that it is stale. This was a convention first,
and it failed twice — a machine ran 0.1.0 while main was at 0.4.0, and the 0.4.0
bump had to be asked for during review; main was still carrying two un-released
plugin commits when the check was written.
**What the check does not catch** — the same list the script header and the ledger
row carry, because a partial list is an overclaim: it verifies that a bump is
*present*, not that it is right. It does not check: semantic correctness (a patch
where a minor was due passes); direction (any different string passes, including a
decrease); anything outside a `pull_request` event (a direct push to main bypasses it
entirely); deletion of an entire plugin directory; any change to which directory the
marketplace entry
points at (rename, copy, or `source` repoint); two PRs branched from the same version
each bumping to the same new one; and whether the version was ever released or tagged.

### Prompts and scaffolding

Expand Down Expand Up @@ -172,11 +204,12 @@ 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 && sh plugins/dev-workflow/hooks/codex-gate.test.sh && sh scripts/check-invariants.test.sh && sh scripts/check-invariants.sh && claude plugin validate . --strict` |
| 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 && sh 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` |
| 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` |
| test | `sh plugins/dev-workflow/hooks/codex-gate.test.sh` |
| invariant checks (5 pinning, 6 manifest) | `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` |
| build | n/a — nothing is compiled or bundled |

**Prerequisites and pinning.** The quality command needs `shellcheck` (0.11.0 locally;
Expand All @@ -189,5 +222,14 @@ disable: every other shellcheck rule still applies to that file. Its hits are al
mode is a broken stdout — which runs `fail` as well, producing a spurious FAIL rather
than a false pass. Revisit if `pass`/`fail` ever gain logic that can legitimately fail.

**`check-version-bump.sh main` has a precondition**, unlike everything else in the
battery: it compares *commits*, so run it once the work is committed (the Gate-B WIP
commit is the natural point) and against a base ref that is current. Run mid-loop with
the plugin edits still in the working tree, it reports clean — correctly, and
uselessly. CI passes the PR's own base ref instead of `main`. It is in the quality row
because that row claims to be what CI runs, and as of invariant 12 that includes this.

CI runs the parts as separate steps for readable failures; the chained form above
is the single command a human runs.
is the single command a human runs. One difference is deliberate: CI's version-bump
step is `pull_request`-only, while the local battery always runs it (on `main`, where
the merge-base is HEAD, it passes trivially).
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,17 @@ 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 both executables and their test
files, the hook's test suite, the invariant checks
([`scripts/check-invariants.sh`](scripts/check-invariants.sh) and its own suite), and
`claude plugin validate . --strict`. The single command that runs the whole battery
locally is in [`AGENTS.md`](AGENTS.md) under "Commands".
PR and push to main: `shellcheck --shell=sh` over all three executables and their test
files, the hook's test suite,
[`scripts/check-invariants.sh`](scripts/check-invariants.sh) (invariants 5 and 6) plus
both checkers' regression suites, and `claude plugin validate . --strict`.

A fifth check runs **on pull requests only**:
[`scripts/check-version-bump.sh`](scripts/check-version-bump.sh) (invariant 12), which
needs a base branch to diff against. Its *suite* runs unconditionally with the others;
the checker itself does not, so a commit pushed straight to main is never version-bump
checked. The single command that runs the whole battery locally is in
[`AGENTS.md`](AGENTS.md) under "Commands".

Everything else here is a **prompt**, and prompts have no typechecker — they are
reviewed against [`docs/prompt-standards.md`](docs/prompt-standards.md), this repo's own
Expand Down
5 changes: 4 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ README.md, MANIFEST.md, AGENTS.md, CLAUDE.md, todos.md
.claude-plugin/marketplace.json
.github/workflows/ci.yml # lint + hook tests + invariant checks + validate
scripts/check-invariants.sh # invariants 5 and 6, mechanically (+ .test.sh)
scripts/check-version-bump.sh # invariant 12, PR-only, mechanically (+ .test.sh)
plugins/dev-workflow/
.claude-plugin/plugin.json
CHANGELOG.md # every manifest version, newest first
skills/{intake,harden-finding}/SKILL.md
agents/finding-triage.md
commands/{workflow-init,process-pr-review}.md
hooks/{hooks.json,codex-gate.sh,codex-gate.test.sh}
examples/ # read, don't install — one stack's answers
examples/ # ships, but never scaffolded — one stack's answers
docs/{getting-started,coding-workflow,prompt-standards,architecture}.md
docs/{hardening-log,hardening-taxonomy,pr-review-bots}.md
docs/superpowers/{specs,plans,stories}/ # approved artifacts behind past changes
source-files/ # the extraction seed this repo was built from
```

Expand Down
Loading