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
10 changes: 10 additions & 0 deletions .changeset/lucky-donkeys-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@cosyte/cli": patch
---

Gate the two-file agent-guidance contract with `pnpm check:agent-notes`, run from the test suite so
it rides the required CI contexts and `prepublishOnly`. The narrative file must be tracked, no
section may be emptied down to its heading, and every pointer at it must resolve. The matcher was
derived by counting both pointer spellings on this tree rather than ported from a sibling, a bare-form
census refuses if the spelling this scope rests on ever changes, and the gate refuses at exit 2
rather than reporting green over a corpus it never opened.
72 changes: 72 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,78 @@ still do. Each entry was assigned to the release whose tag first contains it, re

## [Unreleased]

### Added

- **The two-file agent-guidance contract is now gated (`pnpm check:agent-notes`).** `CLAUDE.md` was
split from `documentation/agent-notes.md` on 2026-08-04, which made every anchor between them
load-bearing, and nothing checked them. `scripts/check-agent-notes.ts` now verifies that the
narrative file is tracked, that no section is emptied down to its heading (a container whose body is
its subsections is exempt and counted), and that every pointer at it resolves. It runs from
`test/scripts/agent-notes.test.ts`, so it rides the required `ci / verify` contexts and
`prepublishOnly` rather than adding a fourth workflow.
- **The matcher was derived by counting this tree, not by porting a sibling's.** Two pointer
spellings are live across these repositories, and porting without re-counting has produced a false
green twice: a qualified-only matcher would have covered 3 of `ncpdp`'s 38 pointers, and would
have matched nothing at all in `terminology` (42 bare, zero qualified) while still exiting 0.
**Measured here: every pointer is the qualified `documentation/agent-notes.md#<anchor>` form and no
bare pointer exists**, so the gate matches that form only. No count is written into the gate's
prose as a promise; the OK line prints all of them on every run, because it measures rather than
remembers.
- **A bare-form census keeps that scope honest.** Matching one spelling is safe only while the other
stays absent, so in **every opened file** each backticked `#<anchor>` span is enumerated: a
digits-only one is a pull-request reference and is counted and reported, and **any other one
refuses the run at exit 2** with an instruction to re-derive the matcher. Refusal rather than a
finding, because the tree has not necessarily broken but the evidence the scope rested on has.
A first draft scoped the census to the pair and justified that with tree files it claimed a
widening would red; **running it tree-wide proved none of them can, and that the only files that
could were the gate's own source and test**, which wrote bare spans out literally. That made the
narrow scope a self-exemption for the very files where a broken pointer would hide, so the samples
are now assembled from parts and the census covers the corpus. It closes the hole a pair-scoped
census leaves: a bare pointer in a third file was covered by neither the matcher nor the census.
- **Three claims were withdrawn after review rather than shipped.** The opening promise said the
gate catches "a rename"; it does not, because both halves match on **basename** and the directory
is never compared, so moving the file while pointers keep their prefix exits 0 with every rendered
link broken. That is now a narrowed promise and a disclosed miss rather than a bigger guard. A
**heading inside an HTML comment** mints a phantom anchor, which the list had omitted; it is now
disclosed and pinned. And a written-down count of sibling repos lacking the file was already
wrong, so **the number was dropped in favour of the class**.
- **Two further claims were withdrawn on later review passes, each arriving inside the fix for the
one before it.** The first fix asserted the HTML-comment miss is disclosed in "every sibling
copy"; that phrasing came from a review rather than from the tree, and it is false. `ncpdp`,
`terminology` and `astm` disclose it; `mllp`, `ccda`, `transform` and `docs` do not, and `mllp`
is the copy this gate's disclosed-miss block is transcribed from, so the shared ancestor never
carried the entry. The second fix then said the four that lack it are all owed it. Also false:
`mllp`, `ccda` and `transform` derive anchors by **slugging headings** and are owed it, while
`docs` resolves explicit `<a id>` anchors and cannot have the miss at all. **A claim about
another repository is not checkable from inside this one**, so it is measured against that
repository's source or not made.
- **It refuses rather than reporting green over a corpus it never opened.** There is no declared
scan root to be wrong about: the corpus is `git ls-files`, reconciled as **sets of paths**, and
zero pointers, zero tracked files, an unmerged path, a symlink, a non-regular file or two files
carrying the contract basename each refuse at exit 2. This repository has already shipped the
opposite defect once, in `phi-scan`, which printed `OK, no hits` over a root it never walked.
- **Every claim was watched to fail on a clone of the real tree** before the gate was believed: a
misspelled real anchor, an emptied real section, the narrative file deleted, a bare pointer that
resolves, a neutered matcher, and a tree with every pointer rewritten out of range. The last two
are the `terminology` scenario, and both refuse instead of going green.
- **Exit codes and corpus handling were re-derived from this repository, not inherited.** `0`, `1`
for a finding and `2` for a refusal come from `scripts/phi-scan.ts`. An unmerged path **refuses**
here even though `phi-scan` leaves that status unenumerated, because its reasoning (`git commit`
refuses an unmerged index) covers a staged route this gate does not have.
- **The NUL skip is a disclosed miss, not a pass**, and is required rather than tidy: the tree
tracks vendored `@cosyte/*` tarballs and a synthetic DICOM fixture, none readable as markdown or
editable to clear a red. The tell is the skipped count on the OK line. A draft claimed this
partition differs from `check-no-emdash.sh`'s; **that was false and was corrected by reading that
gate's own OK line.** Both key on an actual NUL byte; the wider set is git's own binary
classification, which is why neither gate may be reduced to `grep -I`.

### Changed

- **`CLAUDE.md` narrative was relocated into `documentation/agent-notes.md` to make room for the gate's
rules.** The branch-protection, PHI-scanner-residual and em-dash blocks were compressed to their
imperatives; every trap keeps a one-line rule and a pointer, and the reasoning each one compresses
was already in the narrative file. No trap was deleted and no ceiling was raised.

### Fixed

- **`pnpm phi-scan`'s all-mode walk was rooted at `test/__fixtures__` and `src` only, so 89 of this
Expand Down
107 changes: 53 additions & 54 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
> imperative with a link to the section that proves it. **"I did not read the reason" is not a licence
> to discount the rule.** Every one of these lines cost a defect to learn.

> **The pair is gated** (`pnpm check:agent-notes`, enforced by `test/scripts/agent-notes.test.ts`, so
> it rides `ci / verify` and `prepublishOnly`): the narrative file must be tracked, every section must
> have a body (a container's is its subsections), and every pointer at it **in a file it opened** must
> resolve. **A NUL-bearing file is skipped: a disclosed miss, not a pass**; the tell is the skipped
> count, and **only the BASENAME is compared, so moving the file to another directory keeps this
> green while every link 404s**. It matches the **QUALIFIED spelling only**; a **bare** backticked
> anchor anywhere **REFUSES the run**: the measurement that scoped the matcher has gone stale.
> **Never port a sibling's matcher without re-counting both spellings here.** It asserts **this
> repo's promise, not a universal**, and **refuses (exit 2) rather than reporting green over a
> corpus it never opened**. **Never clear a red by deleting the pointer or the heading.** Why:
> [agent-notes § The gate](documentation/agent-notes.md#the-two-file-contract-gate).

## Project

**`@cosyte/cli`**: the **cosyte CLI**: a **`bin` package** (the `cosyte` command), not a parser and
Expand Down Expand Up @@ -183,15 +195,13 @@ Why: [agent-notes § The pre-commit PHI gate and git mv](documentation/agent-not
- **`scripts/` IS SWEPT: name a PHI shape, never SPELL a literal** (a draft banner red the gate on
itself). The SSN check reads **no** allow-list, so an `ID` in dashed shape reds
`phi-allow-list.txt`: respell `MRN-`.
- **🛑 THE WIDENING BOUGHT THE SSN/EMAIL FLOOR OVER 38 MORE FILES AND NOTHING ELSE** (all hand-read:
enumeration gap, not exposure). **The recogniser was NOT widened, on measurement** - this floor is
anchor-free, so it never had the "file IS the document" defect, and an escape-decoded view finds
nothing new. **A tripwire reds if that changes**; widen **in addition to** the raw pass.
- **Other residuals:** `D` and `U` are unenumerated (`U` costs nothing that can reach a commit:
`git commit` refuses an unmerged index, exit 128); the routes now differ widely: `--staged` is
`test/__fixtures__` + `src/*.ts` only, CI sweeps the rest.
- **Give `test/scripts/phi-scan.test.ts` explicit timeouts.** Each case spawns `tsx` cold: 0.5s idle,
**3.7s under contention**, against a shared 10s default.
- **🛑 THE WIDENING BOUGHT THE SSN/EMAIL FLOOR OVER 38 MORE FILES AND NOTHING ELSE** (an
enumeration gap, not exposure). **The recogniser was NOT widened, on measurement**, and **a tripwire
reds if that changes**: widen **in addition to** the raw pass, never instead of it.
- **Other residuals:** `D` and `U` are unenumerated (`U` reaches no commit: `git commit` refuses an
unmerged index, exit 128); the two routes now differ widely.
- **Give every `test/scripts/*.test.ts` case an explicit timeout.** Each spawns `tsx` cold: **3.7s
under contention**, against a shared 10s default.
- **Assert the premise, not only the remedy.** Two vacuity traps already sprang in this suite: a
fixture whose `git merge` refused on an identity-less runner so every later assertion held over an
empty result, and a loop that asserted only the detection-OFF side.
Expand Down Expand Up @@ -226,14 +236,13 @@ Why: [agent-notes § The em-dash brand gate](documentation/agent-notes.md#the-em
seeding each with a live em dash. A red on those echoes a whole synthetic message into a public CI
log: deliberate and acceptable.
- **Fix the script's shared known limits in the shared copies, not here.** A divergent copy is worse
than a shared known limit. The script is composed from three siblings' fixes; understand the
composition before editing it.
- **Re-derive every number before writing it down and never quote one from a sibling's copy.** The
first draft of these notes stated a wrong count _inside the sentence arguing for measurement rigor_.
than a shared known limit; the script is composed from three siblings' fixes.
- **Re-derive every number before writing it down and never quote one from a sibling's copy.** A draft
of these notes stated a wrong count _inside the sentence arguing for measurement rigor_.
- **What lands on `main` here differs from `mllp`: all three merge methods are enabled**, so the
**branch commit messages** are the one text that lands under every method, the PR title lands under
two of three, and **the PR body lands under none** (it is scanned anyway, as deliberate
over-strictness). **Do not repeat `ncpdp`'s copy** claiming the title and body are what lands.
**branch commit messages** are the one text that lands under every method and **the PR body lands
under none** (scanned anyway, deliberate over-strictness). **Do not repeat `ncpdp`'s copy** claiming
the title and body are what lands.
- **Not retroactive:** commit subjects already on `main` may carry `U+2014`. History is not rewritten.

## Tech Stack (the shared `@cosyte/*` standard)
Expand Down Expand Up @@ -272,41 +281,33 @@ a summary.
Full ruleset, the required-context table and the per-check reasoning:
[agent-notes § Branch protection](documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim).

- `main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`). Before it
existed every check here was advisory, on the branch that publishes. **Seven** required contexts,
each pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy
it. **Do not quote that count without re-deriving it** (`gh api repos/cosyte/cli/rulesets/19907924`):
it was `6` until `ci / prepublish` was added, and it moves when the called workflow does.
- `main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`); before it
existed every check here was advisory, on the branch that publishes. Every required context is
pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy it.
**Never quote the context count without re-deriving it**
(`gh api repos/cosyte/cli/rulesets/19907924`): it moves when the called workflow does.
- **Read a required context off a REAL check run, never off a workflow's `name:` field.** Requiring a
context nothing emits does not fail a PR: it leaves it **pending and unmergeable forever**, with no
error and no warning.
- **▶ A `ci / *` CONTEXT CAN APPEAR HERE WITH NO COMMIT IN THIS REPO, AND IT ARRIVES NOT REQUIRED.**
`ci.yml` calls `cosyte/.github/.github/workflows/ci.yml@main` unpinned, so a job added upstream
starts emitting a context here that the ruleset does not name: **a red X that does not block a
merge**, which is the failure this repo's whole protection claim exists to prevent. `ci / prepublish`
arrived that way on 2026-08-05 and was unrequired until measured and added. **Census `ci / *` against
a real check run whenever `.github` moves**, then require it or write down why not, in `ci.yml`'s
banner.
- **`no-internal-refs` and `no-emdash` are bare JOB IDS**, not `<workflow> / <job>`: they are ordinary
jobs in this repo's own workflows. **Renaming the job silently detaches the required check.** Rename
the job and the ruleset together, or neither.
- **A required job gates all of its steps.** Splitting a step out of `ci / verify` into its own job
silently un-requires it. There is a banner on `ci.yml` where someone would trip it.
- **Never add a `paths:` filter to `ci.yml`, `codeql.yml`, `no-internal-refs.yml` or
`no-emdash.yml`.** None carries one, which is what stops a PR skipping a required check.
- **Confirm a ruleset write with the `PUT` itself, never a `GET`.** An Organization-sourced ruleset
returns `200` to a `GET` and `404` to an identical-payload `PUT`. `19907924` is
`source_type: Repository` and is the only ruleset this repo has.
context nothing emits does not fail a PR: it leaves it **pending and unmergeable forever**.
- **▶ A `ci / *` CONTEXT CAN APPEAR HERE WITH NO COMMIT IN THIS REPO, AND IT ARRIVES NOT REQUIRED**
(`ci.yml` calls the reusable workflow unpinned), so an upstream job shows **a red X that does not
block a merge**. **Census `ci / *` against a real check run whenever `.github` moves**, then require
it or write down why not.
- **`no-internal-refs` and `no-emdash` are bare JOB IDS**, not `<workflow> / <job>`. **Renaming the job
silently detaches the required check**: rename the job and the ruleset together, or neither.
- **A required job gates all of its steps**, so splitting a step out of `ci / verify` into its own job
silently un-requires it. **Never add a `paths:` filter** to `ci.yml`, `codeql.yml`,
`no-internal-refs.yml` or `no-emdash.yml`: none carries one, which is what stops a PR skipping a
required check.
- **Confirm a ruleset write with the `PUT` itself, never a `GET`**: an Organization-sourced ruleset
answers `200` to a `GET` and `404` to an identical-payload `PUT`.
- **Never require `scorecard / analysis`, `fuzz`, or `release / release`**: none runs on
`pull_request`, so each would strand every PR pending forever. **Never require the Advanced Security
`pull_request`, so each strands every PR pending forever. **Never require the Advanced Security
`CodeQL` check** (app id `57789`): it reports **alert state**, not whether the analysis ran.
- **▶ Scope of the claim: a ruleset makes a red check BLOCK a merge. It does not make the check
correct, and nothing inside this repository can observe its own ruleset.** Delete it and this suite
stays green while this section keeps asserting protection. Verify the only way that works:
`gh api repos/cosyte/cli/rulesets`.
- Recorded **unproven** rather than fine: no fork PR has ever run here, so neither the
first-time-contributor gate nor whether `codeql / analyze` can report on a fork token has been
observed.
correct, and nothing inside this repository can observe its own ruleset.** Verify the only way that
works: `gh api repos/cosyte/cli/rulesets`. Recorded **unproven** rather than fine: no fork PR has
ever run here, so neither the first-time-contributor gate nor whether `codeql / analyze` can report
on a fork token has been observed.

## Engineering Guardrails

Expand Down Expand Up @@ -342,16 +343,14 @@ return 0`, so no `--profile`, `--ignore-rules` or config setting reaches that ea
- **`--profile node16` is load-bearing and is forwarded, never reinterpreted.** Without it
`@cosyte/cli/mcp` fails `node10` resolution.
- **Only a TOTAL loss of declarations is the false green; a PARTIAL one `attw` catches itself**, so
the preflight must report both outcomes and **must not assert the exit 0**. Six packed-but-undeclared
declarations decide which silence you get, and the obvious two-line version of this is false: a
refuter falsified a first draft of it in one run. **Re-measure before you shorten it.**
the preflight must report both outcomes and **must not assert the exit 0**. The obvious two-line
version of this is false: a refuter falsified a draft in one run. **Re-measure before shortening.**
- **The post-check reads a string, so what would hide that string is refused**: `--quiet`, `-q`,
`--format`, `-f`, `--config-path`, and a `.attw.json` setting `quiet` or `format`. **Say "exact argv
token" of the ARGV refusal, never "wholesale"** (the stronger wording was live and was refuted); the
`.attw.json` refusal **is** wholesale, and the two messages differ on purpose.
token" of the ARGV refusal, never "wholesale"** (the stronger wording was refuted); the `.attw.json`
refusal **is** wholesale, and the two messages differ on purpose.
- **Two holes are disclosed and deliberately left open**: clustered short forms `-fjson` / `-Pf json`,
and a declared path not starting with `.`. Left open on purpose; the reasoning is in the linked
section.
and a declared path not starting with `.`. The reasoning is in the linked section.
- `test/scripts/attw-gate.test.ts` pins both nets, the upstream exit 0 itself, a negative control, and
that a real `attw` failure still fails with `attw`'s own status.

Expand Down
Loading
Loading