Skip to content

feat(make-claim-liveness): principle 36 — a script's make <target> claim must be true - #1366

Merged
artyhoo merged 4 commits into
stagingfrom
claude/clever-kowalevski-975fb2
Aug 10, 2026
Merged

feat(make-claim-liveness): principle 36 — a script's make <target> claim must be true#1366
artyhoo merged 4 commits into
stagingfrom
claude/clever-kowalevski-975fb2

Conversation

@artyhoo

@artyhoo artyhoo commented Aug 10, 2026

Copy link
Copy Markdown
Owner

What

tests/consumer-matrix/python-unfamiliar-stack-cell.sh:39 claimed it runs under make consumer-matrix. It never did — Makefile:34-38 invokes only the pnpm-monorepo and npm-tarball cells. Same class as the ci-success needs-completeness defect: a header asserting a coverage property about itself that no mechanism verifies, and which is false. Different surface, so it was deliberately left out of that PR per the CLAUDE.md no-drive-by rule.

Two things ship here: the claim is corrected, and the class becomes mechanically detectable.

The decision: (b) correct the header, not (a) wire the cell in

Not the cheap option — the reason (a) is wrong is specific, and it is about host safety rather than cost.

The cell does not degrade when ast-grep is absent. It installs it: python-unfamiliar-stack-cell.sh:346 runs npm install -g "$ASTGREP_PKG" unconditionally. That lands in npm prefix -g/bin. Verified on a stock Homebrew macOS host:

probe result
npm prefix -g /opt/homebrew → writes to /opt/homebrew/bin
readlink /opt/homebrew/bin/ast-grep ../Cellar/ast-grep/0.44.1/bin/ast-grepbrew-owned symlink
ls /opt/homebrew/lib/node_modules/@ast-grep absent — the existing binary is not npm-managed

So make consumer-matrix — the documented local-verify entry point — would either fail on the collision or replace a brew-managed binary. Neither sibling cell does any global install; this is the only one that mutates state outside its tmpdir. A local-verify target must not do that to an operator's package manager.

The header now records the trigger to revisit: scope the install to the cell (npm install --prefix "$WORK/tools"), and it becomes host-safe and should join the target. That keeps (a) available rather than closing it off.

Incidental finding while checking (a): the cell's Node-strip loop removes every PATH dir containing node/npm/npx. On this host that is /opt/homebrew/bin, which also holds python3 and git — both survive only because /usr/bin fallbacks exist. Recorded, not acted on; the cell is CI-only, where the strip is exercised as designed.

The gate: principle 37

packages/core/principles/37-make-target-claim-liveness.test.ts — every backticked make <target> in a comment line of tests/**/*.sh + scripts/**/*.sh must name a target whose resolved recipe actually invokes the claiming file.

Resolver is reused, not rebuilt. make -n <target> is make judging itself: $(CURDIR) expansion, @-silenced lines and transitive prerequisites all come free (make -n self-audit resolves all three prerequisites). A Makefile regex would be a second copy of make's semantics, free to drift — the failure dual-implementation-discipline.md exists to prevent. A precondition arm asserts the Makefile stays free of include / $(shell …), the two constructs that make -n unsafe.

False-positive risk was measured, not assumed — this was the reason to weigh before building. Over the real corpus:

anchor matches false positives
bare make \w+ 17 14 (ordinary prose — "would make every…", "must make the counter bite")
backticked `make <target>` in a comment line 3 0

Population is only 3 sites today, which argues against a gate — except attention had already failed on 1 of those 3, through a stage PR and its review. Both existing cells got their header by copy-paste, which is exactly how the false one was born.

Channel: principlesMetaSection() at pre-push.ts:1267 already runs test:principles, so this fires at pre-push, not only CI — no new hook machinery.

Escape hatch: # make-claim: allow <reason ≥20 chars>, mirroring ci-tool-pin: allow (ci-tool-pinning.md §3). It covers documenting a target's absence; the corrected header is its first production consumer.

Paired negatives

arm input expected
N37-1 the shipped defect, reconstructed RED
N37-5 same claim, file the target does run GREEN (anti-tautology)
N37-2 escape token, real rationale GREEN
N37-3 escape token, TODO / bare RED
N37-4 nonexistent target RED
N37-6 make x on an executable line not a claim

Plus a vacuity guard: the live corpus must yield ≥3 claims, so a silently-broken regex cannot make the positive arm pass trivially.

End-to-end mutation proof. Reverting the header makes the live gate fail with:

tests/consumer-matrix/python-unfamiliar-stack-cell.sh:39 — claims `make consumer-matrix`
runs it, but that target's resolved recipe never invokes
tests/consumer-matrix/python-unfamiliar-stack-cell.sh

…with the other 9 arms still green. Restoring the header returns 10/10.

Verification

§1.7 Forward-check applied

Capability-commit gate: new file packages/core/principles/37-make-target-claim-liveness.test.ts is ≥80 LOC under packages/, so it is a capability commit — carries a Prior-art: trailer citing new SSOT row prior-art-evaluations.md:319 (#246), landed in the same commit per CLAUDE.md. Verdict HYBRID is already in the registered set at 11-build-first-reuse-default.test.ts:89, so no VERDICTS extension was needed and principle 11 passes unmodified. Research per build-first-reuse-default.md §3: context7 on GNU Make + WebSearch ×3 phrasings; checkmake/shellcheck/lychee surveyed, T16 problem-class mismatch recorded in the SSOT row. no-paid-llm-in-ci holds — the gate is execFileSync + regex, zero API calls. Escape-token shape follows the precedent at ci-tool-pinning.md §3, and the comment-line carve-out mirrors unpinned-tool-install.ts:75 rather than inventing a second convention.

§1.7 Backward-check applied

Class = an artifact asserts a falsifiable property about a channel it does not own, and nothing verifies it. Complete enumeration of surfaces where that class occurs, each probed:

  1. Shell scripts claiming a make targetgrep -rnoE 'make [a-z-]+' tests/ scripts/ → exactly 3: pnpm-monorepo-cell.sh:46 SWEPT-CLEAN (true), npm-tarball-cell.sh:41 SWEPT-CLEAN (true), python cell GAP — fixed here. Now gated.
  2. ci-success needs-completenessGAP-FOUND at sweep time, CLOSED UPSTREAM before merge. When this sweep ran, scripts/ci-success-gate.sh:6 claimed the job "needs: every audit-self PR job" while shipped-prettier (audit-self.yml:99) and framework-fresh-install-validate-multistack (audit-self.yml:1094) were absent from needs:. fix(ci-success): wire the two unwired jobs, and gate the wiring so it cannot drift again #1362 landed on staging mid-branch and fixed exactly that, gate included (packages/core/principles/36-ci-needs-completeness.test.ts). Merged forward — nothing left to do here. The finding is kept rather than deleted because it is what makes this PR's class claim non-hypothetical: two instances of the class were independently gated within an hour, by sessions unaware of each other.
  3. Rule-doc Class: A — companion test shipped at <path> claims — every path extracted from .claude/rules/*.md resolves on disk (0 missing). SWEPT-CLEAN today, but verified by nothing; latent instance of the same class.
  4. run-local-ci-sweep.sh COVERED/UNREACHABLE lists (run-local-ci-sweep.sh:20-55) — prose claims about which CI jobs a green sweep predicts. Unverified by construction; already a known open item, untouched here.
  5. setup.d/** — deliberately OUT of scope: shipped into consumer repos that have no Makefile of ours, so a make-target claim there would concern a different build file. Documented in the test header, not silently omitted.

Exemption mechanism: the make-claim: allow sentinel, with its own arms — N37-2 proves the exemption preserves intent, N37-3 proves removing the rationale breaks it, so the hatch is not a blanket bypass (37-make-target-claim-liveness.test.ts:290).

Fidelity verdict

FIDELITY: skipped — direct operator request, not a stage PR: there is no kickoff or spec substrate to audit WHAT-conformance against, and this body declares no ## Provenance. The self-assessment below stands on its own evidence.

Conforms. Scope as asked: the false claim is fixed, the fix is the reasoned option rather than the cheap one, and the general problem was weighed on measured evidence (0/3 vs 14/17 false-positive rate) before the gate was built. Paired negatives ship with the check, as required. One deviation from a literal reading of the brief: the scope is tests/**/*.sh plus scripts/**/*.sh — same class, same anchor, zero current hits there, cheap to include and it forecloses the obvious next copy-paste. Surface 2 of the backward sweep was a real defect of this exact class, found by the sweep and then closed upstream by #1362 before this PR merged — it is not covered by principle 37 and did not need to be. Two collisions with that PR were absorbed here rather than papered over: the SSOT row renumbered twice (244 → 245 → 246, stepping aside for #1362 and #1368) and this gate renumbered principle 36 → 37. Staging absorbed three merge-forwards during this branch; each conflict was a pure ID collision, resolved by yielding the number to whichever row was already merged.

Follow-ups (not in this PR)

  • Make the python cell host-safenpm install --prefix "$WORK/tools"; unlocks option (a) and lets the cell join consumer-matrix.

Prior-art: prior-art-evaluations.md#246 (GNU make -n as target-recipe resolver + the absence of any tool verifying a script's prose claim about which build target invokes it — verdict HYBRID: ADOPT make -n as the resolution engine, BUILD only the claim-extraction slice. checkmake, its MegaLinter descriptor, shellcheck and lychee surveyed; T16 problem-class mismatch recorded — checkmake is single-file syntactic, this check is cross-file semantic. Restated here because Prior-art: commit trailers are dropped at squash.)

Test added 4 commits August 10, 2026 12:14
…claim must be true

tests/consumer-matrix/python-unfamiliar-stack-cell.sh:39 claimed it runs under
`make consumer-matrix`. It never did — the target invokes only the pnpm-monorepo
and npm-tarball cells. Same class as the ci-success needs-completeness defect: a
header asserting a coverage property about itself that no mechanism verifies, and
which is false.

The claim is corrected to CI-only rather than made true by wiring the cell into the
target. This cell is the only one of the three that mutates host state OUTSIDE its
tmpdir: :346 runs `npm install -g @ast-grep/cli@0.44.1`, landing in `npm prefix -g`/bin
— on a stock Homebrew macOS that is /opt/homebrew/bin, where ast-grep is already a
brew-owned symlink into Cellar (verified on the host: `npm prefix -g` = /opt/homebrew,
no npm-managed @ast-grep under lib/node_modules). A documented local-verify target must
not collide with the operator's package manager. The header records the trigger to
revisit: scope the install to $WORK and the cell becomes host-safe, at which point it
should join the target.

Principle 36 makes the class mechanically detectable at pre-push (principlesMetaSection
already runs test:principles) rather than leaving it to a reader — the #hope-as-gate
anti-pattern that let the original claim ship. It reuses `make -n` as the resolver, so
make judges itself: variable expansion, @-silenced lines and transitive prerequisites
all come free, and no second copy of make's semantics can drift. A precondition arm
asserts the Makefile stays free of `include` / `$(shell …)`, the two constructs that
would make `-n` unsafe.

Anchor measured before shipping, not assumed: backticked `make <target>` in a comment
line scores 0 false positives on the real corpus, against 14/17 for a bare `make \w+`
regex over ordinary English prose. Escape hatch `# make-claim: allow <reason ≥20 chars>`
mirrors the `ci-tool-pin: allow` token and covers the legitimate case of documenting a
target's ABSENCE — the corrected header is its first production consumer.

Paired negatives (principle 02): N36-1 false claim → RED (the shipped defect
reconstructed), N36-5 true claim → GREEN (anti-tautology), N36-2 escape → GREEN,
N36-3 short-rationale escape → RED, N36-4 nonexistent target → RED, N36-6 executable
line → not a claim. Verified end-to-end: reverting the header makes the live gate fail
naming `python-unfamiliar-stack-cell.sh:39`, and restoring it returns 10/10 green.

Prior-art: prior-art-evaluations.md#244 (GNU make `-n` as target-recipe resolver + the
absence of any tool verifying a script's prose claim about which build target invokes it
— HYBRID: ADOPT the resolver, BUILD only the claim-extraction slice. checkmake, its
MegaLinter descriptor, shellcheck, lychee and the comment-consistency literature all
surveyed; T16 problem-class mismatch recorded — checkmake is single-file syntactic,
this check is cross-file semantic).
…245)

Merge-forward per .claude/rules/git-conflict-merge-forward.md (rebase / force-push
is a dead end on a published branch).

Sole conflict: this branch and #1362 both appended an SSOT row numbered 244.
Resolution — staging's #1362 row KEEPS 244 (already merged, its own trailer cites
that ID); this branch's GNU-make-resolver row is renumbered to 245 and ordered after
it. Verified post-resolve: no conflict markers, no duplicate IDs across the table,
sequence ends 243 · 244 · 245, markdownlint 0 errors, and the diff against staging
is a single added line (the first resolve attempt also ate the blank line before the
closing `---`, which turned the whole table into a setext heading — MD003 at :186;
restored).

CORRECTION to 4e4231a's trailer: it cites `prior-art-evaluations.md#244`, which
after this merge is #1362's actionlint row. The correct citation for principle 36 is
**#245**. Principle 11 F1 still resolves via the verbatim SSOT path match (row 245's
"Capability matched" cell names
packages/core/principles/36-make-target-claim-liveness.test.ts), and the squash body
at merge time carries #245.

Two staging commits landed mid-branch and each retires something this PR asserted:

- #1362 wired `shipped-prettier` + `framework-fresh-install-validate-multistack` into
  `ci-success` `needs:` and gated it — exactly the GAP-FOUND in this PR's §1.7
  Backward-check surface 2. Now CLOSED upstream; the PR body is updated so it does
  not go on asserting a defect that no longer exists.
- #1363 sized the spawn-heavy hook suites to their real runtime — the local
  `test:hooks` flake this branch diagnosed (12 failures, all "timed out in 5000ms",
  all under hooks/; the same file passes 5/5 in 22.76s under
  --no-file-parallelism --testTimeout=30000).

Note for the reviewer: #1362 independently BUILT a needs-completeness gate for the
sibling surface while this PR built the make-target-claim gate. Two instances of one
class — "an artifact asserts a falsifiable property about a channel it does not own"
— were gated within an hour of each other, by different sessions, neither aware of
the other. That is corroboration that the class is real, not a one-off.

Prior-art: prior-art-evaluations.md#245 (GNU make `-n` as target-recipe resolver +
the absence of any tool verifying a script's prose claim about which build target
invokes it — HYBRID: ADOPT the resolver, BUILD only the claim-extraction slice).
…aken by #1362)

Second collision from the same merge. #1362 landed
`packages/core/principles/36-ci-needs-completeness.test.ts` on staging while this
branch was in flight, and its SSOT row 244 names "principle 36" in its retirement
trigger. That slot is now spoken for, so this branch's gate takes 37.

Renamed via `git mv` (history preserved) and swept for stale references rather than
renaming the file alone: header prose, both `describe` titles, the in-test
"principle 36 must re-justify its mechanism" self-reference, all 10 `N36-*` arm ids
→ `N37-*`, and the verbatim path in SSOT row 245's "Capability matched" cell — which
is the cell principle 11 F1 path-matches against, so a stale path there would have
silently broken the prior-art resolution this branch depends on.

`grep -rn "36-make-target\|N36-\|Principle 36 —"` over both files → no hits.
`npx vitest run principles/` → 39 files, 370 passed, 1 skipped; principle 36
(#1362's) and principle 37 (this branch's) both green side by side.

Trailer note: this commit takes a POSITIVE citation, not the escape hatch. The
pre-push substance gate is right to demand it — `git mv` + a content edit registers
as a new ≥80-LOC file under `packages/`, and the CLAUDE.md relocation carve-out
("byte-identical to a blob already tracked elsewhere") does NOT apply here, because
the rename also rewrote the 36 → 37 references inside the file (15 insertions,
15 deletions). The first attempt used `Prior-art: skipped` and was correctly blocked.

Prior-art: prior-art-evaluations.md#245 (GNU make `-n` as target-recipe resolver +
the absence of any tool verifying a script's prose claim about which build target
invokes it — HYBRID: ADOPT the resolver, BUILD only the claim-extraction slice. This
commit relocates that same capability to slot 37; the SSOT row's "Capability matched"
cell is updated in this commit to the new path.)
Third merge-forward on this branch; staging is moving fast today. #1368 appended its
own row 245 while this branch held that number. Resolution is the same shape as the
244 → 245 one: the row already ON staging keeps its ID (its commit trailer cites it),
and this branch's GNU-make-resolver row steps aside — now **246**.

Verified post-resolve: no conflict markers, no duplicate IDs across the whole table,
sequence ends 244 · 245 · 246, the blank line before the closing `---` is intact (its
loss during the first resolve turned the table into a setext heading — MD003), and
markdownlint reports 0 errors.

Trailer bookkeeping: commits 4e4231a and bf5ef84 cite `#244` and `#245`
respectively; after this merge the row is `#246`. Both citations are stale by
renumber, not by substance — no verdict, rationale or evidence changed, only the
row's position behind two upstream inserts. Principle 11 F1 resolves via the verbatim
SSOT path match (row 246's "Capability matched" cell names
packages/core/principles/37-make-target-claim-liveness.test.ts), which has been kept
correct across every renumber, and the squash body at merge time carries #246.

Prior-art: prior-art-evaluations.md#246 (GNU make `-n` as target-recipe resolver +
the absence of any tool verifying a script's prose claim about which build target
invokes it — HYBRID: ADOPT the resolver, BUILD only the claim-extraction slice).
@artyhoo
artyhoo merged commit f39124f into staging Aug 10, 2026
57 of 62 checks passed
@artyhoo
artyhoo deleted the claude/clever-kowalevski-975fb2 branch August 10, 2026 10:40
artyhoo pushed a commit that referenced this pull request Aug 10, 2026
…sion with #1366)

`gh pr list --state open` shows PR #1366 (branch claude/clever-kowalevski-975fb2,
opened 2026-08-10T09:28:32Z) already adding
`packages/core/principles/37-make-target-claim-liveness.test.ts`. It has the
earlier claim on the slot, so this branch moves to 38 rather than making the two
collide at merge. No registry or contiguity assertion exists over principle
numbers (`ls packages/core/principles` + grep found none), so a gap would have
been harmless anyway — but two files claiming one slot would not be.

Renames the file and updates every reference: the test's own header, the
discipline-self-check.yml and workflow-integrity.yml comments that name the
enforcing gate, the run-local-ci-sweep.sh block header, and SSOT #245. Also drops
a stale line in the discipline-self-check.yml header that still described the
guard as `base=staging` after 9c00b25 changed it to `!= 'main'`.

Trailer note: this is a rename, not a new capability — but the detector counts
the new path as a ≥80-LOC add under `packages/`, and correctly refuses the escape
hatch for it (the byte-identical carve-out does not apply, since the file's own
header text changed 37→38). Citing the entry the artifact has carried since it
was introduced is the accurate answer, not a bypass.

Prior-art: prior-art-evaluations.md#245 (actionlint RuleGlob / zizmor / poseidon/wait-for-status-checks / blend/require-conditional-status-checks / GitHub-native required_status_checks API — verdict BUILD. Unchanged by this commit: the capability is the same required-context registrability gate, moved from slot 37 to slot 38.)
artyhoo added a commit that referenced this pull request Aug 10, 2026
…ger, and gate registrability (#1371)

## Summary

Completes the half #1368 declared and explicitly deferred, and closes the gap that deferral left in the gate.

#1368 (merged 10:26Z today) built the declaration model: every job declares `# required-context: yes|no` at the job, and principle 37 asserts both in-repo lists equal the declared set. It marked `discipline-self-check.yml`'s job `no`, naming the precondition in the marker itself: «To make it required, the path filter must first move from `on.pull_request.paths` into the job body … so the context always reports.» The 2026-08-10 operator ruling requires that context on `staging`, so the precondition had to be met.

Two things here: the §1.7 context becomes registrable and is declared required, and principle 37 gains the arm that makes «declared required ⇒ actually reportable» a mechanism instead of a comment.

**Scope note, stated plainly:** this branch was opened before #1368 existed (my in-flight probe ran at 09:33Z; #1368 was created at 09:39Z) and originally carried its own parallel list-gating design. That work is discarded — #1368's declare-at-the-job model is better, and this PR is rebased onto it. The net diff is 5 files.

## Changes

**1. `discipline-self-check.yml` — registrable, and declared required.** The `paths:` trigger filter is removed, and the marker flips to `# required-context: yes`. GitHub's guidance is the reason the filter and the requirement are incompatible: «Workflows skipped due to path filtering, branch filtering, or commit messages will remain in a pending state and block merging. To avoid this, do not require workflows that can be skipped.»

Removed rather than reimplemented in the job body. §1.7 already has a declared out-of-scope mechanism — the `### §1.7 Skipped: <≥60 chars>` marker, which `.github/pull_request_template.md:61` emits as the DEFAULT for every PR. A scope list inside the job body would have been a third hand-maintained copy of «what counts as discipline-bearing», in a PR whose subject is that such copies drift. **Measured before removing it:** of the 40 most recent merged base=staging PRs, 38 already carry a valid §1.7 section (11 Skipped markers, 27 Forward/Backward pairs). #1363 and #1337 are the only two that would newly redden, each needing the one template-default line.

Also adds `if: base.ref != 'main'`: a promote PR aggregates ~90 squashed commits and has no single meaningful forward/backward pair, so today it gets a guaranteed RED. Deliberately not the siblings' `== 'staging'` — theirs is subject-scoped (a kickoff's fidelity verdict, a stale-base rebuild against staging), whereas §1.7 also applies to the base=`epic/*` PRs `CLAUDE.md:121` permits agents to merge, and `== 'staging'` would have silently dropped that coverage. Safe because a job skipped by `if:` reports `skipped`, which protection accepts — «if a job is skipped due to a conditional, it reports success».

**2. Both derived lists gain the newly-declared context**, per #1368's rule that the three move together. The sweep's `REGISTRATION STATE` line is also rewritten to name contexts instead of «the last three» / «the first two»: appending a sixth entry above it silently falsified both phrases — the same positional-drift shape principle 37 exists to catch.

**3. Principle 37 arms (i)–(k) — registrability.** The existing arms assert the declared set is *restated consistently*. None asserts a declared entry is *safe to register at all*: `hasPullRequestTrigger` (`:112`) checks only that a `pull_request:` trigger EXISTS, never that it is unfiltered. So flipping any marker to `yes` inside a `paths:`-filtered workflow passes every existing arm and then freezes every PR matching none of the filter. That is exactly the hazard #1368 wrote into the `discipline-self-check.yml` marker and deferred — and prose is not a mechanism (`.claude/rules/attention-is-not-a-mechanism.md:1`).

- **(i)** every `required-context: yes` job sits behind an unfiltered `pull_request:` trigger (no trigger at all is equally an offender).
- **(j)** paired negative — re-adding the removed filter must be detected.
- **(k)** parser scope: `pull_request:` only (`workflow-integrity.yml` filters *both* `push:` and `pull_request:`, so a file-wide scan would look identical here); inline `on: [push, pull_request]` resolves to `[]` rather than a false «no trigger» RED; job-level `if:` is never flagged, because flagging it would forbid the promote-flow guards all three PR-body gates rely on.

Added to principle 37 rather than shipped as a parallel principle: that file already owns the declaration model and its parsers, so a second principle would have had to duplicate them.

## Operator action required

**Order matters — register only AFTER this merges.** `discipline-self-check.yml` on `staging` is still `paths:`-filtered until then; registering its context first deadlocks every non-matching PR.

Live state re-read 2026-08-10: `{"strict":false,"checks":[{"app_id":15368,"context":"ci-success"},{"app_id":15368,"context":"fidelity-verdict-in-pr-body"},{"app_id":15368,"context":"stale-revert-in-pr-diff"}]}` — three registered against six now declared.

```bash
gh api -X PATCH repos/artyhoo/getff/branches/staging/protection/required_status_checks --input - <<'JSON'
{
  "strict": false,
  "checks": [
    {"context": "Template render probes — P1/P4/P6 (deterministic)", "app_id": 15368},
    {"context": "capability PR carries Prior-art line in PR body (squash-survival)", "app_id": 15368},
    {"context": "ci-success", "app_id": 15368},
    {"context": "fidelity-verdict-in-pr-body", "app_id": 15368},
    {"context": "stale-revert-in-pr-diff", "app_id": 15368},
    {"context": "§1.7 forward+backward sections present in PR description", "app_id": 15368}
  ]
}
JSON
```

- Payload generated from the declared set, not retyped; `strict: false` + `app_id: 15368` reproduce the current pinning. Passing bare `contexts[]=` would drop the app pinning.
- This registers all six declared contexts, including the two #1368 left declared-but-unregistered. Register a subset if you want them staged separately — but every unregistered declared context is one that can go red while the PR merges.
- **PRs open at registration time will not report the new checks until they get a fresh PR event** (a push or a body edit — all the relevant workflows carry `types: [… edited …]`).
- Verify: `gh api repos/artyhoo/getff/branches/staging/protection --jq '.required_status_checks.contexts[]'` → 6 lines.

**Effect on the 6 currently-open PRs, measured (`gh pr list --state open`):** #1366 carries a valid §1.7 section. The other five — #1331 plus four Dependabot bumps (#1230, #1217, #1216, #1149) — do not, and will show a red §1.7 once this merges. This does **not** create a new blocked class: those four are *already* blocked by an existing required check — `gh pr checks 1230` shows `fidelity-verdict-in-pr-body  fail` today, because Dependabot's body carries no `## Fidelity verdict` either, and Dependabot cannot edit its own body. Whatever already handles that for these PRs handles this too. Worth an explicit decision eventually; out of scope here.

## Test plan

- [x] `npx vitest run packages/core/principles/37-required-context-completeness.test.ts` → 11/11 (8 pre-existing + 3 new).
- [x] **Paired-negative on disk, not just in-parser.** The `paths:` filter was re-added to the real `discipline-self-check.yml` and the suite re-run: arms (i) and (j) both RED, with the deadlock message naming the job. Restored → 11/11 green.
- [x] `npm --prefix packages/core run test:principles` → 40 files, 381 passed, 1 skipped, 0 failed.
- [x] `bash scripts/run-local-ci-sweep.test.sh` → ALL PASS (the sweep's own paired-negative, after editing its header block).
- [x] `actionlint` on both changed workflows → clean; `bash -n scripts/run-local-ci-sweep.sh` → clean; `npm run format:check` → clean.
- [x] Full `bash scripts/run-local-ci-sweep.sh` on the pre-merge tree: PASS through `vitest-composition`; `format-check`, `rule-index-check`, `alwayson-budget`, `test:render`, `test:ir` also green individually.
- [x] Live branch protection re-read before writing the operator command.

**Pre-existing failures on this host, NOT introduced here** (both reproduced on a clean `staging` checkout, so out of scope per CLAUDE.md «PR strategy»):

- `packages/core/hooks/getff-work.test.ts` — 3 failures (`CC-DEFERRAL`, `NON-CC-PRINT`, `NO-LAUNCH-FLAG`, all `expected 1 to be +0`). Verified identical on `/Users/art/code/rules-as-tests-aif` at `staging`.
- `packages/core/principles/20-bundle-classification.test.ts` — one 30 s timeout under full-suite parallel load; passes in isolation in 45 s (~3.3 s/test), and green in the 40-file run above. Same spawn-heavy class #1363 addressed.

## Provenance

n/a — not a stage PR. Direct implementation of the 2026-08-10 operator ruling, rebased onto #1368 after that PR merged mid-flight.

## Review findings

n/a — not a stage PR. Three findings came from this branch's own pre-handoff self-review and were fixed before handoff: the `!= 'main'` guard scoping (`== 'staging'` would have dropped epic/* coverage); the inline `on: [push, pull_request]` grammar in the arm-(i) parser (an unhandled inline form would have false-REDded the one unconditionally safe shape); and the sweep's positional «last three» claim, which this PR's own sixth list entry falsified.

**Also surfaced, not fixed** (no drive-by, per CLAUDE.md «PR strategy»): `origin/staging` currently carries **two** principle-37 files — `37-make-target-claim-liveness.test.ts` (#1366) and `37-required-context-completeness.test.ts` (#1368) — merged within minutes of each other. Nothing asserts principle-slot uniqueness, which is the same missing-mechanism class as everything else in this PR.

## Fidelity verdict

FIDELITY: skipped — no kickoff or spec governs this change; it implements a direct operator ruling recorded in the task brief, not a stage of a planned umbrella.

## Parked questions

n/a — not a stage PR.

## §1.7 Self-discipline check (REQUIRED if PR touches discipline-bearing files)

### §1.7 Forward-check applied

**`no-paid-llm-in-ci.md §2`** — arms (i)–(k) are `readFileSync` + regex, zero API-billed calls; the workflow edits remove a trigger filter and add no step (`packages/core/principles/37-required-context-completeness.test.ts:71` imports only `node:fs`/`node:path`/`node:url`). **`ci-tool-pinning.md §1`** — no bare `run:` tool install added or altered; `.github/workflows/discipline-self-check.yml:79` still has no install step. **`attention-is-not-a-mechanism.md §1`** — this is the rule's own case: the deferred fix lived as a prose marker comment on a job, i.e. detection resting on someone reading it; arm (i) replaces it with a deterministic gate, and the surviving best-effort job at `workflow-integrity.yml:32` stays explicitly labelled non-load-bearing. **`rule-enforcement-channel-selection.md §3`** — mechanically detectable → gate; a principle test is the earliest firing channel (`packages/core/hooks/pre-push.ts:1269` runs `test:principles` at pre-push, CI as backstop). **`build-first-reuse-default.md §3`** — DeepWiki `rhysd/actionlint` + context7 `/github/docs` ×3 phrasings + WebSearch ×2 ran before the BUILD verdict; recorded as SSOT #247 in `docs/meta-factory/prior-art-evaluations.md:319` and cited by the commit trailer. The same rule drove the *shape*: the arms were folded into the existing principle 37 rather than shipped as a parallel principle, reusing its declaration model and parsers instead of duplicating them. **`language-discipline.md §1`** — all added comments, test prose and commit bodies are English. **CLAUDE.md capability-commit gate** — the diff adds no new file ≥80 LOC and no dependency; the trailer cites the SSOT entry anyway, since the capability is real even though the artifact is an extension.

### §1.7 Backward-check applied

Delegated cold to `agents/backward-sweep-auditor.md` per T21 — handed only the two class predicates, never the diff, branch or PR, so it could not restate this change. (Dispatched against the pre-merge tree; both classes are structural and unaffected by the rebase, and finding B-2 below is about files this PR does touch, so it was re-checked by hand against the merged state.)

**Class A — «a hand-maintained list whose correct contents are determined by a population declared elsewhere, with nothing asserting the mirror».** Population 13, all reached. SWEPT-CLEAN: `packages/core/principles/27-*` (install.sh copy-list ⊇ import graph), `.claude/rules/00-rule-index.md` + AGENTS.md (`scripts/render-rule-index.mjs --check`, `packages/core/hooks/pre-push.ts:1253`), agents/rules/skills header coverage (`packages/core/principles/09-doc-authority-hierarchy.ts:228`), `tests/agnosticism` probes (`run-audit.sh:14` globs, no hand list), `.claude/hooks/**` channel markers. **Six GAP-FOUND, none in this diff:** (1) the Tier-0 core set exists in three copies — `packages/core/principles/31-rule-channel-declaration.ts:58` (3 entries) vs `scripts/render-rule-index.mjs:56` and `scripts/render-rule-channels.mjs:75` (2 each), the latter's comment at `:70` claiming a «verbatim» mirror while differing, and `--check` regenerates *from* the stale copy so it stays green; (2) `packages/core/principles/09-doc-authority-hierarchy.ts:28` `REQUIRED_HEADER_DOCS` is static, and 5 `docs/meta-factory/*.md` carry no header; (3) `tests/install-sh/meta-all-wired.test.sh:23` loops only `tests/install-sh/*.test.sh`, leaving 29 of 127 tracked `*.test.sh` unasserted, 13 invoked by nothing; (4) `setup.d/LAYERS.md:3` declares itself authoritative for the layer list but carries 13 rows against 16 globbed by `install.sh:1142`; (5) `setup.d/10-skills.sh:108` names 12 of 14 skills, in sync but unasserted; (6) `scripts/run-local-ci-sweep.sh:123` gate table (low severity — a stale row yields a false LOCAL green and gates no merge).

**Class B — «a declared gate whose channel cannot fire where it is relied upon».** Population 9, all reached. SWEPT-CLEAN: `guard-liveness-fullsweep.yml:20`, `pr-body-prior-art.yml:28`, `audit-self.yml:549` lychee vacuity guard, `audit-self.yml:1058` toolchain non-vacuity, `scripts/host-verify-coverage.sh:4`. `workflow-integrity.yml`'s own `paths:` filter is clean precisely because it is declared `required-context: no` — which arm (i) now asserts rather than assumes. **Three GAP-FOUND:** (1) 113 of 262 `packages/core` test files execute at no channel — `packages/core/vitest.config.ts:9` allowlists 22 dirs, pre-push runs 4 `test:*` scripts and CI 7, so `validator/gate-conflict.test.ts` and six other self-described gates never run; `audit-self.yml:389` states the class in the repo's own words and `:394` defers it; (2) all three PR-body required gates skip on base=`epic/*` while `CLAUDE.md:121` permits agents to merge there; (3) changed-markdown link integrity is pre-push-only and degrades to a warning when lychee is absent (`packages/core/hooks/pre-push.ts:1499`), while `link-checker.yml:26` is paths-filtered to `lychee.toml`.

**Acted on within scope:** B-2 is the one finding this PR's own files carry, and it is fixed here for the §1.7 gate — `discipline-self-check.yml:77` uses `!= 'main'`, so base=`epic/*` stays covered. The cold agent reached that defect independently, on the two sibling gates, without seeing the diff; the two siblings keep `== 'staging'` and are **not** changed here, since narrowing them is a different subject with a recorded prior decision (`docs/superpowers/specs/2026-07-23-acceptance-contour-design.md:161`). The other eight findings are sibling surfaces outside this umbrella and are surfaced, not fixed — the Tier-0 three-copy set is the closest structural twin of what principles 36 and 37 each fixed once.

Prior-art: prior-art-evaluations.md#247 (actionlint RuleGlob / zizmor / poseidon/wait-for-status-checks / blend/require-conditional-status-checks / GitHub-native required_status_checks API — verdict BUILD, narrow complement to #245. actionlint validates `paths:` glob SYNTAX but per DeepWiki on `rhysd/actionlint`, 2026-08-10, «does not analyze the implications for branch protection» and «does not have a feature to cross-check a list of status-check context strings against job `name:` fields»; the marketplace answers to skipped-but-required are RUNTIME waiters that add a job to wait on other jobs, never a static assertion that a declaration is registrable.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant