# Changelog

All notable changes to `@elvatis_com/aahp` are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/), and the project uses semantic
versioning (`aahp_version` in `MANIFEST.json` tracks the file-format contract and moves
independently of the npm version).

> Publishing note, measured 2026-09-28 against `git ls-remote --tags` and
> `npm view @elvatis_com/aahp versions`:
>
> - `3.1.0`, `3.3.0`, `3.4.0`, `3.8.2` and `3.8.3` have a section below but were never
>   tagged and never published to npm. They were version numbers the repository passed
>   through between releases; their changes first reached npm in `3.2.1` (for `3.1.0`),
>   `3.5.0` (for `3.3.0` and `3.4.0`) and `3.9.0` (for `3.8.2` and `3.8.3`). Their
>   reference links point at the commit that set the version, not at a tag.
> - `3.2.0` was tagged but never published to npm; `3.2.1` was published instead.
> - npm releases lapsed after `3.2.1` because the CI publish workflow was disabled, so
>   `3.5.0` is the first npm release since `3.2.1` and ships everything below it.
> - `3.0.0` to `3.0.4` were published to npm before this changelog existed and have no
>   sections of their own; the `3.0.5` entry summarizes the 3.0 line.

## [Unreleased]

## [4.0.0] - 2026-09-28

### Added

- `assets/governance/aahp-verify.yml`, the adopter verify workflow: `npm ci
  --ignore-scripts`, then `aahp verify --level ci` and `aahp doctor` run by path from
  `node_modules/`. Install it with `scripts/propagate.sh`, or copy it from the installed
  package.
- `tests/install-hooks.bats`, and propagate tests that run every `run:` step of the
  installed workflow in a real npm consumer.
- `npm run check:bats-negations` (`tests/assert-bats-negations.mjs`): fails on a `!`
  negation that is not the last command of a test or function.
- `scripts/run-bats.mjs`: with `CI` set, fails the run on any skip not listed in
  `ALLOWED_SKIPS`, and refuses a first-on-PATH bash older than 4.1 unless
  `AAHP_ALLOW_OLD_BASH=1`.
- Red-control tests for `assert-pinning-gate-wired.mjs`, `assert-doc-shape-wired.mjs` and
  `assert-workflow-parser-parity.mjs`; `tests/README.md` with the suite's rules, covered
  by `tests/bats-hygiene.bats`.
- `aahp manifest --force` regenerates even when the existing MANIFEST.json cannot be read
  or holds fields that cannot be carried over (that data is dropped).
- `aahp migrate --yes` / `-y` for non-interactive regeneration of an existing
  MANIFEST.json.
- `aahp --help` and README document archive's Python 3 requirement, `--keep N`,
  `--verify`, lint options and every `verify --level`.
- Verify Layer 4 executable trust claims: a `TRUST.md` `Check` column names a built-in
  check (`license-matches`, `manifest-integrity`) or a reviewed `trustTtl.checks` entry
  (argv, no shell, 120 s limit). A check-backed `verified` row is judged by its check on
  every run, not by its date. Nothing read from `TRUST.md` is ever executed.
- `trustTtl.graceDays` (default 14): a date-judged `verified` row warns from its expiry
  and blocks under `trustTtl.enforce` only after the grace period. Layer 4 prints a census
  of the rows it read.
- Opt-in `handoffImpact.npmDevDependencyUpdates`: for Layer 2, a change touching only
  `package-lock.json` (optionally with devDependency specifier changes in `package.json`)
  is non-impacting when every changed entry is `dev: true`, integrity-pinned, resolved
  from registry.npmjs.org and carries no install script. It is decided by content, never
  by author, and requires a `supplyChainScan` assertion that the gate re-proves on every
  run.
- `check-workflow-pinning.mjs` rules H (a shipped template pins the same SHA and version
  as the workflows), I (`npm ci --ignore-scripts`), J (`npx` only after `npm ci` in the
  same job) and K (grouped, cooled-down Dependabot lanes); rule B also rejects `npm exec`
  and `npm x`.
- `docs/adr/`: the Architectural Decision Log, one file per decision (`ADR-NNN.md`) plus
  an index, and `docs/governance.md`, the reference for `aahp doctor`, `aahp check` and
  the config-driven gates. Both moved out of README.md, which keeps the quickstart and the
  normative specification.
- `npm run check:adr-refs` (`scripts/check-adr-refs.mjs`, repository-local): every ADR
  number cited in a tracked file has a file, and each ADR's heading and index row match
  its file name.
- Forbidden-pattern rules `no-internal-hostnames` and `no-estate-counts`, and a
  CONSTITUTION section on what public handoff state and documentation may record.
- CONTRIBUTING.md documents the handoff update every pull request needs, Dependabot
  handling, how to run tests, and the release ceremony (moved from README Section 11).
- `scripts/check-ascii.mjs` (`npm run check:ascii`, part of `npm run check`): every
  tracked text file must be pure ASCII, with no code point above U+007F and no byte order
  mark. It reports `file:line:column` and the code point, exempts binary files (NUL byte),
  and exits 1 on a finding and 2 when it cannot assess. This replaces the U+2014-only rule
  as the enforcement of the ASCII-only invariant; a cp1252 Windows console cannot encode
  characters such as U+2705 or U+2192.
- `aahp archive --reindex` records a deliberate edit (a redaction) of `LOG-ARCHIVE.md` in
  `LOG-ARCHIVE.index.json`: it prints every hash it drops and records, writes nothing when
  nothing changed, touches only the index, and refuses to combine with `--verify`.
- `aahp init --gates` writes `.github/workflows/aahp-verify.yml` (the adopter copy) when
  `.ai/handoff/` exists, and otherwise prints how to add it later. It never writes it into
  the `@elvatis_com/aahp` package itself.
- `scripts/validate-json-schema.mjs`: validates JSON files against a JSON Schema with ajv
  (draft 2020-12, ajv-formats full mode, ajv's default strict mode). Exit 0 valid, 1
  invalid (every error with its JSON Pointer), 2 could not evaluate. It ships in the
  package; adopters who want it declare `ajv` and `ajv-formats` themselves (README Section
  2.1).
- `aahp doctor` gains a `cli-source` gate (`scripts/check-cli-source.mjs`, ADR-025). It
  reports workflow steps that fetch the aahp CLI from the registry at run time
  (`registry-fetch`), run the unscoped name `aahp` through a package runner
  (`unowned-name`), run `node bin/aahp.js` in a repository without that file
  (`checkout-path`), or call it from `node_modules/` with no install step earlier in the
  job (`no-install`). Each fails doctor, except `npx --no-install aahp` after an install,
  which fails closed and is `advisory`. The package itself reports `self`; `gates` gains
  the `cli-source` key (`total` 8, `schemaVersion` stays 2).
- `aahp init --gates --workflows` rewrites only the workflow files `init --gates` manages
  (aahp-govern.yml, and aahp-verify.yml where `.ai/handoff/` exists, never in the package
  itself) and leaves `aahp.config.json` and `package.json` untouched; `--workflows`
  without `--gates` is refused.
- `aahp migrate` removes optional MANIFEST.json task fields that hold a template
  placeholder the schema rejects (`"[ISO-8601]"`, `"YYYY-MM-DDT00:00:00Z"`) and prints
  task, field and old value; a required field holding one stops the run with nothing
  changed, and a regeneration `aahp manifest` refuses restores the original bytes.

### Changed

- `propagate.sh` requires the target to declare `@elvatis_com/aahp` and to have a
  `package-lock.json` in the git index that locks it; otherwise it exits 3 (new exit code)
  before writing anything.
- The npm package no longer ships AAHP's own `.github/workflows/aahp-verify.yml`, which
  cannot run outside an AAHP checkout. Migration: a consumer that copied it should replace
  it with `node_modules/@elvatis_com/aahp/assets/governance/aahp-verify.yml`.
- Migration: hooks previously installed from a linked worktree were inert; re-run
  `install-hooks.sh`.
- The test fixture repository is built once per bats run and copied per test: setup cost
  per test drops from about 22 to 7 ms on Linux and from about 595 to 201 ms on Windows.
- `aahp manifest` requires Node.js and builds MANIFEST.json in one node process;
  generation for this repository on Windows went from 9.3 s to 0.8 s.
- One token estimator for every tier (the larger of 1.3 tokens per word and 1 token per 4
  bytes), and `token_budget.manifest_only` is measured from the generated file instead of
  the constant 85.
- `check-conflict-markers.mjs` scans git-listed files plus `.ai/handoff` and exits 2 when
  it cannot run.
- `aahp doctor`'s manifest-schema gate validates the whole
  `schema/aahp-manifest.schema.json` (enums, lengths, additional keys, RFC 3339 dates).
  A manifest with template placeholder dates such as `"[ISO-8601]"` now fails doctor, as
  it already failed ajv; `aahp migrate` removes them (see the Migration note below).
- Migration: a `generate.log` config with no `target`, or with a target on the agent
  journal, now fails; point `generate.log.target` at a separate file such as
  `docs/RELEASES.md`.
- Layer 3 reports OK when `last_session.commit` is an ancestor of HEAD and HEAD differs
  from it only under `.ai/handoff/`.
- Verify messages name the concrete regeneration command instead of `/handoff`; the
  summary names the failing layer(s) and each layer's remedy, and a checksum mismatch
  advises inspecting `git diff -- .ai/handoff` before regenerating.
- The `trustTtl` config reader refuses duplicate JSON keys and unknown `trustTtl` keys;
  `handoffImpact` needs `nonImpactingModifiedFiles` or `npmDevDependencyUpdates`;
  `templates/TRUST.md` tables gain a `Check` column.
- `tests/verify.bats` builds its fixture once (about 36% faster on Linux).
- Dependabot opens one grouped version-update PR per ecosystem with a 7-day cooldown;
  security updates are neither grouped nor delayed.
- The scanner contract test asserts invariants instead of a literal SHA, so v6 Dependabot
  bumps no longer fail by construction; a v7 bump still does.
- The bats suite runs once per Node runtime per push instead of three times.
- CodeQL actions v4.37.9 to v4.38.2 (Dependabot #123).
- CONSTITUTION marks each rule Enforced (naming its gate or test) or Convention. The ASCII
  rule's rationale is corrected: cp1252 cannot encode the emoji and arrows the templates
  shipped, while the em dash is encodable.
- STATUS.md is a bounded snapshot and LOG.md the only journal. On a conflict, rewrite the
  STATUS snapshot from both sides and keep both LOG entries. `.gitattributes` no longer
  sets `merge=union` for STATUS.md.
- LOG.md entries may be redacted for confidentiality and are never otherwise rewritten.
- Templates are ASCII only. WORKFLOW Phase 5 regenerates the manifest and runs `aahp
  verify`; DASHBOARD is a derived display surface; GROUNDING's field list maps onto the
  TRUST.md columns.
- `aahp init` next steps mention the `[VERSION]` placeholder, the hooks and the adopter
  verify workflow.
- Consumers are told to pin an exact version; the advice to float a caret range is
  withdrawn.
- `scripts/lint-handoff.sh` prints ASCII status marks, `OK`, `x` and `!`, instead of check
  marks, crosses and a warning sign; the final line is `All checks passed.`. Anything that
  matched the old glyphs must match the message text instead.
- `aahp migrate` reports only the optional `<!-- SECTION: summary -->` marker, the one
  AAHP reads (README 1.2).
- The release-journal generator's refusal messages cite docs/adr/ADR-004.md in the AAHP
  repository instead of a README section that no longer exists.
- The `no-estate-counts` forbidden pattern reads every tracked file, not only Markdown.
- `aahp init --gates` scaffolds `"pinnedDep": {}`, so `aahp doctor` fails a range or a
  missing `@elvatis_com/aahp` pin from the first run; a repository with no root
  `package.json` still reports skip. Migration: `init --gates --force` now also overwrites
  `aahp-verify.yml` where `.ai/handoff/` exists.
- A LOG redaction must leave the marker `[redacted: <reason>]` (README 1.3,
  `templates/LOG.md`).
- The `propagate.sh` exit-3 message lists the exact fix commands, and README and ROLLOUT
  state that non-JavaScript repositories adopt AAHP with a `package.json` that only pins
  the tool.
- The `run-bats.mjs` refusal of a bash older than 4.1 names the macOS fix and the probed
  bash path.
- `aahp init --gates` never writes or replaces `aahp-verify.yml` in the
  `@elvatis_com/aahp` package itself (the pinned-dep gate's `self`), even with `--force`:
  the package's own verify workflow runs the gate from the working tree.
- CI validates `MANIFEST.json`, `aahp.config.json` and `aahp.config.example.json` with
  `scripts/validate-json-schema.mjs` instead of `npx --no-install ajv-cli validate`; the
  verdicts match ajv-cli 5.0.0 on a 27-case corpus, and no workflow runs npx any more.
- The release ceremony in CONTRIBUTING.md has an approval step for the `npm-publish`
  deployment.
- Doctor's manifest-schema failure names `aahp migrate` when task fields hold template
  placeholders, and only then.
- Migration: from 4.0.0 a task field left as a template placeholder fails doctor; run
  `aahp migrate --yes` (a required field such as title or status must be set by hand).
- Migration: workflow copies from earlier versions that fetch the CLI at run time, use the
  unscoped name, run `node bin/aahp.js`, or skip `npm ci` fail doctor's `cli-source` gate;
  run `aahp init --gates --workflows`, review the diff and re-apply local edits (steps in
  other workflow files are edited by hand). `aahp init --gates --force` stays the full
  re-scaffold.
- Migration: `handoffImpact.npmDevDependencyUpdates` cannot be added before upgrading,
  because 3.12.0 rejects the key; land the upgrade as one reviewed pull request with the
  version bump, the opt-in (with `supplyChainScan`), a STATUS.md update and a regenerated
  MANIFEST.json (README 5.1).
- The `aahp migrate` prompt and `--yes` cover the placeholder removal; its steps are
  numbered 1 to 6.

### Removed

- `_aahp-lib.sh` helpers `aahp_file_mtime`, `aahp_auto_summary`, `aahp_estimate_tokens`
  and `aahp_file_entry_json`, used only by the old generator.
- README claims with no mechanism or measurement behind them: vendor pricing, reduction
  percentages, the schema as a "hard security boundary", a structural HTML-comment check,
  a `.ai/hooks/lint-handoff.sh` hook, HANDOFF.lock "enforcing single-writer access", and
  "the counter increments automatically".
- An internal hostname and figures about other repositories, from README, ADRs, CHANGELOG
  history and `.ai/handoff/LOG.md` (redacted in place).
- The `ajv-cli` devDependency (5.0.0, whose dependencies include the deprecated
  `glob@7.2.3` and `inflight@1.0.6`) and the `fast-json-patch` override, which nothing
  depends on any more. `ajv` 8.20.0 is a direct exact devDependency; the locked closure
  goes from 28 to 8 packages and `npm ci` prints no deprecation warning.

### Fixed

- `propagate.sh` installed a workflow whose doctor step (`node bin/aahp.js`) failed in
  every consumer with `MODULE_NOT_FOUND`.
- `propagate.sh` vendored `lint-handoff.sh` without `check-conflict-markers.mjs` and
  `validate-pii-allowlist.py`, so every commit reported conflict markers. It now vendors
  the full closure and checks it after copying.
- `propagate.sh` exited 0 when the staged baseline failed verification; it now exits 1,
  and a caller's `AAHP_SKIP_VERIFY=1` no longer skips that check.
- `propagate.sh` rejected linked worktrees; it now accepts them and refuses a
  subdirectory instead.
- `install-hooks.sh` installed hooks where git never runs them (in a linked worktree, and
  with a relative `core.hooksPath` plus a subdirectory target); it now uses `git rev-parse
  --git-path hooks`. It also strips CR, replaces a symlinked hook instead of writing
  through it, and never overwrites an earlier backup.
- `.gitattributes` checks out `scripts/hooks/*` with LF line endings; Linux bash rejects a
  CRLF hook with `set: pipefail: invalid option name`.
- README Quickstart told npm adopters to copy a workflow that only works inside an AAHP
  checkout, and its step 4 output was wrong. Both are corrected, and the global-install
  option is replaced by what it loses.
- Tests: a bare `! grep` in the middle of a propagate test asserted nothing, and the
  actor-bypass guard is now case-insensitive.
- Tests: three `! grep` assertions (archive rotation and two migrate-grounding
  preconditions) could never fail, because bash exempts negated commands from `set -e`;
  they now assert the exit status.
- Tests: prerequisite skips (ajv-cli, python, symlink support) no longer fail open on CI;
  `require_tool` skips locally and fails when `CI` is set.
- Tests: two lint assertions that only checked for missing output also require exit 0 and
  a positive result line, so a crash can no longer pass them.
- Tests: the fixture repository is isolated from the machine's global and system git
  config, from repository variables leaked by git hooks, and from repositories above the
  temp directory (`GIT_CEILING_DIRECTORIES`).
- `aahp manifest` no longer writes invalid JSON or invalid UTF-8 with exit 0 (a TAB, a
  quote, a backslash or a multi-byte character at a truncation point); the file is
  replaced atomically and any failure leaves it byte-identical.
- `aahp manifest` no longer drops `tasks`, `next_task_id`, `cross_repo_ref` or `project`
  silently when node is missing or the existing manifest is unreadable, and a checksum
  failure is no longer written as an empty checksum.
- `quick_context` is no longer escaped twice, `--duration` must be a whole number, and
  `files.*.updated` keeps its date while the checksum is unchanged.
- File summaries skip tables, JSON punctuation, bookkeeping labels, fenced code and HTML
  comments.
- `aahp lint` reads handoff bytes as text: one NUL byte no longer hides a secret or an
  email address, an invalid UTF-8 byte no longer hides a secret, and a scan that cannot
  complete is a violation.
- `aahp lint` scans every handoff file for injection phrases, including MANIFEST.json,
  LOG-ARCHIVE.index.json and pii-allowlist.json and their decoded JSON string values, and
  opens MANIFEST.json as UTF-8 on any console code page.
- A conflict-marker check that could not run is no longer reported as "markers found".
- `templates/MANIFEST.json` validates against the schema after `aahp init` + `aahp
  manifest`, with `aahp_version` 3.0 and the complete phase list.
- `aahp verify`, `lint`, `manifest`, `migrate` and `archive` no longer exit 0 when their
  bash process is killed by a signal; the CLI exits 128 plus the signal number and says
  so.
- `aahp check --json` emits only the JSON record, with every gate `unevaluated` and the
  unknown ids in `config.errors`, when `config.check` names a gate that does not exist.
- `aahp status` prints the real MANIFEST.json line count instead of `?`.
- The release-journal generator refuses `.ai/handoff/LOG.md`, `LOG-ARCHIVE.md`,
  `LOG-ARCHIVE.index.json` or an unset target, and `aahp.config.example.json` points it at
  `docs/RELEASES.md` instead of the agent journal.
- `aahp migrate`: a LOG.md with no entries no longer triggers `integer expression
  expected`, a missing answer on stdin is a clear error, and the script, help and README
  no longer claim it splits LOG.md or adds section markers or TTL columns.
- The CLI help banner and doctor header read `aahp vX.Y.Z: AI-to-AI ...` and `aahp doctor:
  conformance for ...` instead of broken ` -Word` text.
- Tests: the manifest preservation tests fail instead of skipping when tasks,
  `next_task_id` or `project` are lost, and the injection-pattern test proves each pattern
  on its own phrase.
- Layer 2 for a project in a subdirectory of its repository: paths are now
  project-relative; before, every change failed.
- The docs claimed an expired `verified` row automatically downgrades to `assumed`; no
  code did this, and the docs now describe the implemented behaviour.
- Layer 4 date parsing no longer uses awk interval expressions, which old mawk reads
  literally, and Layer 3 never passes a non-SHA `last_session.commit` to git.
- The workflow and pre-push hook headers no longer describe Layer 4 as advisory, and three
  python prerequisite skips in `tests/verify.bats` fail on CI instead of passing silently.
- Portability: no shipped script combines grep's ignore-case and fixed-string flags any
  more. The GNU grep 3.0 of Git for Windows aborts on that combination (exit 134), which
  made the new `license-matches` check reject a correct LICENSE on Windows; a test in
  `tests/bash-portability.bats` now forbids it.
- `npx --no-install` documentation (ADR-013, README 2.1 and 11.1, CLAUDE.md, workflow and
  hook comments, gate text) now matches the behaviour measured on npm 10, 11 and 12: `npx`
  refuses after one metadata request, while `npm exec --no-install` on npm 10 and 11
  downloads and runs the package.
- ADR cross-references after the 3.11.0 renumbering: provenance cites ADR-022, doc paths
  ADR-023.
- CHANGELOG discloses the versions never tagged or published, and reference links that
  pointed at missing tags now point at the version commits.
- HANDOFF.lock recovery reads `HEAD`, not `HEAD~1`, and the acceptance-criteria examples
  in README 8.1 and 8.7 now bind to their task.
- README: the pinned-dep gate reports skip until `pinnedDep` is configured, and
  MANIFEST.json fields are split into generated and hand-maintained.
- `scripts/ROLLOUT.md` describes what `propagate.sh` actually vendors and installs.
- The manifest truncation test places multi-byte characters across the real cut point and
  compares exactly, so a byte-based or UTF-16-unit cut now fails it; before, both passed.
- A JSON Schema section number in a comment of `scripts/aahp-schema.mjs` no longer reads
  as a private IPv4 address to the supply-chain scanner.
- Provenance tests cite ADR-022 and doc-shape tests ADR-023, matching the decision log.
- `aahp lint`'s PII check read `*.md` only. It now also reads the decoded string values of
  every JSON handoff file (`MANIFEST.json`, `LOG-ARCHIVE.index.json`) except
  `pii-allowlist.json`, names the JSON path of each finding, and keeps the allowlist and
  noreply/example exemptions.
- README 2.8 and the config schema description said the npm exemption's scan job needs a
  job-level `if:` naming `pull_request`. The gate checks that the workflow's `on:`
  includes `pull_request`, and checks a job-level `if:` only when the job has one: no
  `if:` passes, `if: always()` is refused.

### Security

- Publish: a `Verify the release ref` step refuses any ref that is not a
  `vMAJOR.MINOR.PATCH` tag equal to the package.json version whose commit is reachable
  from `main`.
- Publish: the job installs nothing, restores no cache and runs `npm publish
  --ignore-scripts`, so no dependency or lifecycle code runs while it holds `id-token:
  write`.
- The supply-chain scan runs on release tag pushes and dispatches, gates both `publish`
  and `release`, and consults the historical threat catalog (`refresh-catalog: true`).
- The required `lint-and-validate` check installs with `npm ci --ignore-scripts` and uses
  a pinned, sha256-verified ShellCheck v0.9.0 instead of apt.
- The npm publish job runs in the `npm-publish` GitHub environment (required reviewer,
  deployment policy `v*` tags only, no admin bypass), so a release reaches npm only after
  an explicit approval. `tests/workflow-hardening.bats` fails when the binding is removed
  or renamed (ADR-019, amended 2026-09-28).

## [3.12.0] - 2026-08-31

### Added

- A least-privilege supply-chain-guard CI job for pull requests and pushes to `main`,
  pinned to the signed v6.0.8 release commit. Its reviewed root policy starts with no
  suppressions, checkout credentials are not persisted, and the job has only
  `contents: read`.
- A portable Node launcher for the repository's locked Bats dependency. `npm test` and
  `tests/run.sh` no longer depend on a global Bats installation or an opportunistic
  registry download, and the same entry point works on Windows and Linux.
- README badges for the enabled supply-chain scanner, the shipped AAHP Govern template,
  and the package's dynamically reported Node compatibility. The existing live AAHP
  Verify workflow badge remains in place.
- Explicit adopter remediation for a bypassable verify workflow and for incorrect
  MANIFEST project names written by versions before 3.9.2.

### Changed

- The three CodeQL actions advance from v4.37.7 to v4.37.8 at the immutable commit from
  Dependabot PR #109.
- Project guidance now states the published Node 22 floor, and workflow-pinning coverage
  treats global npm installs as unpinned installs rather than exempting them.
- Release lockfile metadata is synchronized with the package version again; it had
  remained at 3.10.0 through the 3.11.0 release.

### Fixed

- Secret scanning now passes `--` before grep patterns, so a private-key header beginning
  with hyphens is detected instead of being parsed as a command-line option.
- Windows-specific assumptions in CLI, lint, and manifest tests were replaced with
  portable path, interpreter, and permission handling while retaining their Linux
  assertions.
- Contradictory stale comments around release authorization and trusted publishing were
  removed from the workflow and its shape assertion.
- Python bytecode and `__pycache__` directories are explicitly excluded from the npm
  package. A local validation run had compiled the PII validator before packing and
  exposed that the broad `scripts/` inclusion otherwise published the generated `.pyc`.

### Security

- Removed the unpinned `npm install -g npm@latest` immediately before OIDC publishing.
  Node 24 already supplies an npm version that meets trusted publishing's minimum, so
  the privileged job no longer replaces its package manager from a mutable registry
  target.

## [3.11.0] - 2026-08-23

### Added
- **`trustTtl.enforce` gives verify Layer 4 a failing branch, per repository.**
  Nothing in Layer 4 incremented `FAILURES`, so no number of expired `verified` trust
  rows could change the exit code, and 8 of this repository's own 10 verified rows sat
  expired, one by 16 days, with every gate green. Trust Decay is the mechanism by which
  a claim stops counting as verified and it could not stop anything. Absent or false,
  behaviour is unchanged: expired rows warn and the run passes. True, and they fail it,
  as does a register this reader cannot classify, so the gate cannot be disabled by
  breaking the table rather than editing the config. Opt-in because blocking everywhere
  is the wrong trade: a repository whose register already holds expired rows would turn
  red on its next commit for a file its pull requests never touch. See ADR-024.

- An Installation and Quickstart section in `README.md`, above the architectural
  material. There was no heading at any level matching install, quickstart, getting
  started or setup anywhere in the document, and the single install command sat at 58%
  depth under a heading about decisions. The five steps were each executed against a
  throwaway repository before being written down, including the Layer 3 warning a first
  run produces, so the section describes what the commands do rather than what they are
  expected to do. Issue #74.

- `scripts/check-doc-shape.mjs`, a repository-local gate: a backticked repo-relative
  path in the configured documents has to resolve against the git index, and a required
  heading has to exist before a named anchor. It is deliberately NOT part of
  `aahp check`, so no consumer inherits it, and it is wired into the `check` chain the
  required job runs. It is not a blanket rule over every backticked span: measured on
  this README, 78 distinct path-shaped spans exist and 46 correctly name a file in an
  ADOPTER's tree, so only spans whose first segment is a tracked top-level entry of this
  repository are resolved. Exceptions are declared with an exact occurrence COUNT rather
  than as an allowlist, because the defect and the correct uses are the same string in
  different sentences: a path-level allowlist would have exempted the defect this gate
  exists for. It exits 2 on anything it could not assess. ADR-023.

- The five-field provenance block from Section 2.4 now ships in `templates/LOG.md` and
  `templates/STATUS.md`. The templates carried one of the five, so a repository that
  followed the shipped example produced entries that did not satisfy the section's own
  rule. A `provenance-block` group in `aahp.config.json` binds the field list in
  Section 2.4 to both templates through the existing `schema-doc-sync` gate, so dropping
  a field from either side is red. Issue #86.

- Every `uses:` in `.github/workflows/` and in the shipped
  `assets/governance/aahp-govern.yml` is pinned to a full 40-character commit SHA with
  the release in a trailing `# vX.Y.Z` comment, and `.github/dependabot.yml` declares a
  `github-actions` lane so those pins keep moving. Measured on `main` at `2cdaf48`: 25
  references under `.github/workflows/`, 3 pinned and 22 on mutable major tags, plus 2
  more on tags in the template adopters copy. A tag is a pointer its owner can repoint at
  any time with no diff here to review, and 5 of the 6 required status checks ran on
  those references. `scripts/check-workflow-pinning.mjs` gained rules E and F, which
  assert the two halves together: the pin, and the lane that keeps it current. That gate
  already ran in the required check and already exited 0 over all 22 floating references,
  because rules A to D read only `step.run` and a `uses:` step has no `run:` at all - its
  name promised workflow pinning while its scope was npm packages inside workflows. A pin
  with no update lane does not stay correct, it stops moving, and a missing lane cannot be
  seen from the pull-request count: an unscanned ecosystem and an up-to-date one both
  produce zero pull requests. Recorded as ADR-021.

- `tests/assert-repo-ci-shape.mjs` gained a fifth assertion: the recorded list of publish
  conditions in that file and ADR-019 in `README.md`, which is the record a person
  actually reads, are now compared as sets in both directions. Until now either could be
  edited alone with every check green. No workflow behaviour changes; whether the
  `workflow_dispatch` operand should exist at all remains the owner's open decision, and
  ADR-019 now carries the re-measured numbers behind it.

- `tests/assert-repo-ci-shape.mjs` asserts release authorization in `ci.yml`. The
  `publish` job (npm, `id-token: write`) and the `release` job (the GitHub Release) each
  carried a hand-written `if:`; the two disagreed about what counts as a release, and
  nothing in this repository read either one, so the disagreement was invisible and a
  later edit to publish authorization would have been equally silent. The release
  definition is now written once and both jobs must use exactly it, and every additional
  top-level `||` operand on the publish condition must appear in a literal recorded list.
  The assertion reads the PARSED condition rather than a substring, so reformatting the
  workflow changes no verdict, and a job with no `if:` at all is a failure rather than a
  pass. It runs inside the required `lint-and-validate` check. No workflow behaviour
  changes here: whether the `workflow_dispatch` operand should exist at all is the
  owner's call and is recorded as open, with its options, in ADR-019.

- `aahp doctor` gains a `verify-workflow` gate that answers, from inside a consumer,
  whether the workflow hosting the AAHP gate can skip it. Wrapping the `aahp-verify`
  job in an `if:`, or wrapping the gate step inside it, leaves a REQUIRED status check
  that keeps its name and reports success having evaluated nothing: Layer 1 MANIFEST
  checksum integrity is skipped along with the Layer 2 drift gate, and branch
  protection is satisfied by a verdict nobody produced. AAHP could not see this
  before, because the workflow it ships and `propagate.sh` copies is unconditional,
  so the weakening only ever exists in the consumer's copy. Since the canonical
  workflow's last step runs `aahp doctor`, a repository that has weakened its gate
  now reports it on its own pull requests.
  What is asserted is the consequence ("there exists an event on which this workflow
  concludes success without having run the gate at `--level ci`"), not the file's
  shape. Five findings cover it: `job-conditional`, `job-soft-failing`,
  `ci-step-conditional`, `ci-step-soft-failing` and `no-ci-level`. A repository whose
  workflows never run the gate reports `skip`; one whose workflow hosts the gate but
  cannot be classified reports `fail`, because undecided is not clean. Two shapes are
  deliberately not findings because they fail CLOSED, not green: an `if:` on the
  checkout step alone, and `paths:` filters that stop the workflow triggering.

- **The `verify-workflow` gate now audits `assets/governance/aahp-govern.yml` too**,
  which is the shipped workflow with the widest blast radius and the one it did not
  look at. `aahp init --gates` writes that file into an adopting repository, and a
  governance-only adopter has no `aahp-verify.yml` at all, so it is their entire CI
  backstop. Wrapping its `Run governance gates` step in `if: false` left `aahp doctor`
  reporting `SKIP verify-workflow: no workflow here runs the AAHP verify gate` and
  exiting 0: the same false green the gate exists to stop, one file over. Four
  findings cover the governance shape: `govern-job-conditional`,
  `govern-job-soft-failing`, `govern-step-conditional` and `govern-step-soft-failing`.
  They are judged per SUBCOMMAND, because `aahp check` and `aahp doctor` are different
  gates and the shipped template runs both, so a per-job test reads a file whose
  `aahp check` step alone is wrapped as enforced. A repository that runs the
  governance gate unconditionally and no verify gate gets its own verdict,
  `governance-only`, exit 0, whose pass reason says out loud that nothing there
  compares a handoff checksum. `npm run govern` is deliberately not recognised: what
  that script expands to is not readable from the workflow. Measured against consuming
  repositories before release: every verdict and exit code was identical before and
  after this change, so no consumer turns red without changing anything.
  The gate ships no YAML dependency, because AAHP has no runtime dependencies, so
  `tests/assert-workflow-parser-parity.mjs` holds its block-YAML reader against a real
  parser on every workflow and fixture in this repository, on the fields the audit
  reads and on the resulting findings.

- `npm run check:runtime-support` asserts that the runtimes CI exercises and the
  runtimes `engines.node` publishes are the same set, and that the floor of that set
  still receives security patches. It is a RELATION rather than a list of dead
  versions, so it cannot rot: both sides move together or the gate goes red. Exactly
  one dated constant is kept (`SUPPORTED_FLOOR`, measured 2026-08-21). Pins it cannot
  evaluate - a non-matrix `${{ }}` expression, or `node-version-file` - fail rather
  than being skipped, and a state it cannot classify at all exits 2 rather than 0, so
  "I could not look" never reads as "I looked and it was fine".

- A `runtime-matrix` CI job exercises the whole published range (Node 22 and 24), not
  only its floor. The gate binds that matrix directly: emptying it or cutting it to a
  single entry is red even though the standalone pins in the other jobs would keep a
  global pin list populated.

- `tests/assert-workflow-hardening.mjs` asserts both properties over
  `.github/workflows/` **and** `assets/governance/`, so a workflow added without them is
  a red required check rather than a note in a review. It runs under `npm test`, which
  the required `lint-and-validate` job executes, so it has teeth on every pull request
  with no new CI wiring. It exits 2 rather than 0 on anything it cannot decide: a
  `${{ }}` expression it cannot evaluate, a job delegating to a reusable workflow whose
  steps are not visible, or a scan root that is missing or empty. Scanning zero files is
  reported, never treated as finding nothing wrong. Exemptions for a job that genuinely
  needs the checkout credential live in `CHECKOUT_CREDENTIAL_EXEMPTIONS` inside the
  gate, are reviewed as a code change, and print their reason on every run; the list is
  empty, and that is measured rather than assumed, since no workflow here runs
  `git push`, `git commit`, `git fetch`, `git pull` or `git remote`. See ADR-020.

- `tests/assert-repo-ci-shape.mjs` additionally pins the three job-level elevations by
  name. A job-level `permissions:` REPLACES the top-level block rather than merging with
  it, so once `ci.yml` and `codeql.yml` gained a top-level `contents: read`, deleting a
  job block as redundant would silently strip an elevation whose failure would only
  surface on a release tag. That gate takes the root to assert as an argument, and it no
  longer reads any file unguarded: a recorded workflow the given root does not contain is
  named on stderr as NOT asserted, and a workflow that is present but unreadable,
  unparseable or empty is a failure. Previously an absent workflow threw `ENOENT`, so the
  process exited 1 with a stack trace and none of the gate's own findings - the right
  exit code for the wrong reason, and no message to tell the two apart.

### Changed
- **`aahp doctor`'s conformance record moves to `schemaVersion: 2`, and its summary
  counts gates that RAN.** The footer read `Conformance OK: 7 gate(s), no failures.`
  on a repository where zero of seven gates evaluated anything, because it counted
  the gates that exist. `aahp check`, on the same tree, already said `0 gate(s) ran`,
  so the two commands disagreed about how to describe one emptiness and the one
  positioned as the conformance evidence was the one that overstated. The footer now
  reads `Conformance OK: 5 of 7 gate(s) ran, no failures.`, and a run where nothing
  was evaluated reports `Conformance NOT EVALUATED: 0 of 7 gate(s) ran. This is not a
  pass.` and exits 1, reusing the vocabulary this CLI already had for an invalid
  config rather than inventing a second one for the same state. `--quiet` states the
  overall result instead of printing nothing, on both commands.
  **WHAT CONSUMERS MUST UPDATE.** `gates` is byte-for-byte what version 1 emitted:
  same meaning for every key carried over. NOT the same key set: `gates` gains a
  `verify-workflow` key in this release, and an `advisory` value token with it.
  A reader that switches on `gates`
  needs NO change. A reader that asserts `schemaVersion === 1` must widen to `>= 1`;
  that is the only breaking edge, and it is why the version moved rather than fields
  being added silently. A consumer that runs `aahp doctor . --json` as a CI step and
  reads only the exit code is not affected by the field additions. The exit code itself
  is NOT unchanged: blocking on the new `verify-workflow` gate would move existing
  consumers from exit 0 to exit 1 on a workflow they had not touched, which is why the
  gate ships reporting-only by default in this release, behind `verifyWorkflow.enforce`.
  New in 2: `gateOutcomes` gives each gate a refined `outcome` plus the human
  `reason`, `evaluated` counts the gates that produced a verdict, and `total` the
  gates defined. The refinement is the point. Version 1's `skip` stood for four
  different states at once, so a repository that had adopted governance and one that
  had switched every gate off through `config.check` emitted identical records. The
  outcomes are `pass`, `fail`, `missing`, `self`, `not-applicable`, `deselected` and
  `unevaluated`.

- **`aahp check --json` now reaches the same verdict as `aahp check`.** On one tree
  the text path printed `Governance NOT EVALUATED: 0 gate(s) ran.` and exited 1 while
  `--json` exited 0 with every gate `skip`, because the JSON branch returned above the
  zero-gate test. The machine-readable path is the one a CI tick and a dashboard
  consume, so it was the wrong half to leave green. The verdict is now computed once
  and used by both paths. A repository in which at least one gate runs keeps its exit
  code.

- **`aahp verify` Layer 4 no longer reports a trust register it could not read as
  clean.** `aahp_trust_expired` printed nothing both when no entry was expired and
  when not one row was parsed, and Layer 4 called both of them
  `No expired 'verified' trust entries.` A register whose header no longer names a
  `Status` column, or which holds no table this reader recognises, is now reported as
  `TTL was NOT evaluated`, and a clean result carries the number of entries checked.
  This is not hypothetical: a real, populated register headed `Verified Properties`,
  with an `Expires` column and no `Status` column, held a row eight days past its
  expiry and was reported as clean. Layer 4 remains ADVISORY: it still increments no failure
  count and still cannot fail a build. Whether an expired TTL should ever block is an
  owner decision and is deliberately not taken here.

- README Section 2.4 no longer states a provenance MUST, and no longer states an audit
  trail as a property of the protocol. Nothing enforced either: no gate reads the fields,
  `MANIFEST.last_session` records one agent for the most recent session across the whole
  handoff set rather than per entry, and a handoff set stripped of every provenance line
  passes lint, `verify --level ci` and `doctor` at exit 0, reproduced on a throwaway
  repository. Enforcement was rejected because a retroactive MUST would fail adopters on
  history none of them can change, and this repository's own LOG would fail it too. The
  section now states the conditional version, which is what is true. ADR-022.

- `README.md` names `assets/governance/aahp-govern.yml` as the source to copy for the
  governance workflow. It named a `.github/workflows/` path that does not exist in this
  repository and is not in the published package. The two mentions
  that correctly describe the DESTINATION are unchanged. Issue #74.

- `aahp doctor`'s `handoff-set` gate now names the check it did not run. Its pass
  reason reads `N indexed files present, no strays (content not compared; aahp
  verify Layer 1 owns checksum integrity)`. The gate's behaviour is unchanged, and
  deliberately so: it compares the file SET and the INDEX, while comparing a
  recorded checksum against the bytes on disk belongs to `aahp verify` Layer 1,
  which ADR-011 makes the owner of handoff drift. What was missing is the
  honest-summary treatment `scripts/lint-handoff.sh` received in 3.9.0 (the
  `[3.8.3]` entry below describes it; 3.8.2 and 3.8.3 were never published, and
  3.9.0 is the first release that carries it), where a clean run that could not
  establish integrity stopped printing "All checks passed". Without it, a green
  `handoff-set` line reads as an integrity verdict to anyone who has not read the
  ADR. The new wording reaches ONE surface, and that limit is deliberate: the
  `--json` record carries gate statuses only and no reasons, `--quiet` prints
  nothing for a passing gate, and `--governance` skips the gate without
  evaluating it. A repository that ingests the `schemaVersion: 1` record is
  therefore told exactly what it was told before, which is the compatibility
  choice this entry is making rather than an omission.

- README now states the one configuration in which that split has consequences for
  an adopter: when `verify-workflow` reports `skip`, meaning no workflow in the
  repository runs the AAHP verify gate, and the handoff gates are still evaluated,
  then no automated gate in that repository compares a handoff checksum, and a
  green conformance record is not an integrity signal. Repositories using the shipped
  `aahp-verify.yml` are unaffected: it runs `aahp verify --level ci` before
  `aahp doctor` in the same job, so a drift fails the job before `doctor` runs.
  The README no longer offers `aahp lint` as an equivalent remedy: lint's
  checksum comparison runs only under a Python interpreter and exits 0 when
  there is none, so it can pass silently on a drifted tree where Layer 1 fails.

- `assets/governance/aahp-govern.yml`, the workflow `aahp init --gates` scaffolds
  into consumers, now says in its own header that it runs no handoff-integrity gate,
  that neither of its own steps stands in for one (`aahp check` has no file-set or
  index gate, and its `aahp doctor --governance` step skips the handoff gates
  without evaluating them), that `aahp lint` cannot stand in for one in a workflow
  that sets up Node and not Python, and that a repository which also keeps
  `.ai/handoff/` needs `aahp-verify.yml` beside it. Comment only; the workflow
  itself is unchanged.

- `engines.node` is now `>=22`, was `>=18`. Node 18 reached end of life on 2025-04-30
  and Node 20 on 2026-04-30, so the package publicly claimed support for a runtime it
  could not have security-patched, and every consumer inherited that claim on
  install. Consumers still running Node 18 or 20 will now see an engine
  warning, which is the intended signal.

- CI validates on Node 22 instead of Node 20, in `ci.yml`, `aahp-manifest.yml` and the
  `aahp-verify.yml` reference workflow that `propagate.sh` installs into consumers. The
  publish job stays on Node 24, and the gate now enforces that the release path is
  never older than the build path.

- `yaml` is added as a devDependency (zero transitive dependencies) so the new gate
  PARSES the workflow files. Matching YAML with a regex misreads quoting and block
  scalars, and would report a clean repository it never actually read.

- `.ai/handoff/.aiignore` no longer claims to be enforced, because it is not. The
  correction was first made in `templates/.aiignore` only, and the live copy in
  `.ai/handoff/.aiignore` - this project's own instance, and the file the new lint notice
  names - kept both the false sentence and the withdrawn invitation in the same commit
  that removed them from the template. Both copies now carry the same text, a test
  derives the file list from git rather than naming one path, and a second test fails if
  the two ever diverge. If your repository was scaffolded before this release, your own
  `.ai/handoff/.aiignore` still carries the false header; replace it with the current
  `templates/.aiignore`, or at minimum delete the line claiming CI validates it. No code in
  this repository parses it; the single occurrence of the name in the enforcement path
  excludes the file from the scan. `README` section 2.6 said "CI hook validates that no
  handoff file contains these patterns" and `templates/.aiignore` said "Validated by CI
  hooks and agents before committing". Measured: with `10.0.0.*` and
  `*.internal.example.com` added to `.aiignore`, a committed `STATUS.md` line reading
  `Deploy target: db.internal.example.com at 10.0.0.5` passes `lint-handoff.sh` and
  `aahp verify --level ci`, both exit 0. The damage is the belief, so both claims are
  withdrawn, the "add your internal hostnames, IPs, endpoints here" invitation is
  replaced by the two mechanisms that ARE enforced, and `aahp lint` now prints, in check
  2, how many `.aiignore` patterns it is not applying and by name. Whether the file
  should become a real rule source is left as an owner decision rather than taken here:
  enforcing an existing adopter's committed copy would newly fail their build on
  patterns nobody chose, and the glob vocabulary cannot express what the enforced regexes
  do - the template's `sk-*` has no length floor and matches the word "task-type" inside
  AAHP's own shipped templates, so `aahp init` followed by enforcement is red on an
  untouched repository.

### Removed
- **The publish job no longer accepts a manual dispatch from an arbitrary ref.**
  `publish` runs `npm publish --access public --provenance` with `id-token: write`,
  and its condition carried `github.event_name == 'workflow_dispatch'`, an operand
  that constrains no ref, while the `release` job beside it was tag-only. Two jobs in
  one file disagreed about what a release is and the looser one was wired to the
  registry. The two conditions are now identical. This costs no capability:
  `workflow_dispatch` is still a trigger and a dispatch runs against a chosen ref, so
  selecting a version tag still satisfies the condition and a failed publish can still
  be re-run by hand. Measured first: 206 runs of the workflow, zero manual dispatches.
  See ADR-019.

### Fixed
- **The conflict-marker gate now reads the documents this repository publishes, not
  only handoff state.** It scanned `.ai/handoff/` and nothing else, so a marker in a
  file that SHIPS was invisible. Measured here: a sync merge conflicted in
  CHANGELOG.md, staging with `git add -A` committed the markers, and the pre-commit
  hook, `aahp verify --level ci` and this gate all reported clean while a marker line
  sat in the file that becomes the release notes. Root Markdown documents are scanned
  now, by directory listing rather than `git ls-files` so a non-git project root still
  works, and the OK line carries the count so a scan that reached nothing cannot look
  like a scan that found nothing.

- The governance workflow `aahp init --gates` scaffolds into a consumer repository
  (`assets/governance/aahp-govern.yml`) no longer pins Node 20 - a runtime end-of-life since
  2026-04-30, and below the `engines.node: ">=22"` floor the package publishes. Every
  repository that scaffolded it ran `npm ci --ignore-scripts` and then the AAHP CLI under
  the CLI's own engine floor. Nothing reported it at either end: npm answers an unmet
  `engines` range with an `EBADENGINE` warning and continues, so the adopter's gate stayed
  green while running unsupported. `.github/workflows/aahp-verify.yml` - the file that
  reaches consumers by the other route, `propagate.sh` - has pinned 22 with a comment
  naming the reason since the floor moved; the scaffolded file was never brought along.

- `npm run check:runtime-support` now reads `assets/governance/` as well as
  `.github/workflows/`, which is why the pin above could sit wrong indefinitely. That
  gate exists precisely to assert "no runtime pin is below the published floor", its
  header names the file this package propagates as the reason it exists, and it was
  green on every commit while the shipped template violated it - because AAHP
  propagates by TWO routes and the gate read only the directory covered by one of
  them. Its two sibling gates, `tests/assert-workflow-hardening.mjs` and
  `scripts/check-workflow-pinning.mjs`, already scanned both; this was the odd one out.
  A pin in the shipped template is reported with what actually goes wrong there (an
  adopter's CI running the published CLI below its engine floor) rather than the
  local wording, and it is excluded from the release-vs-build membership check: a job
  that never executes here must not vouch for a release runtime nothing proved.
  Widening a gate's scope must not let it assert less.

- `MANIFEST.json`'s `project` no longer takes the name of the directory the generator
  ran in. It resolves the repository's identity instead: the name already on record in
  `MANIFEST.json` first, then the git remote's repository name, and the directory
  basename only for a genuinely new manifest in a repository with no remote. A
  `git worktree` checkout is named after the branch and a CI workdir after the job, so
  the old behaviour rewrote `project` to that name on every regeneration, silently,
  unless somebody re-read the file afterwards. Two such names reached consumer main
  branches.

- An unsubstituted `[PROJECT]` placeholder, which `aahp init` copies in from
  `templates/MANIFEST.json`, is no longer carried forward as though it were a chosen
  name. It falls through to the remote-derived name.

- The remote-derived name needs only `git`, so the project name is now correct where
  `node` is absent (a stripped hook `PATH`, a slim CI image). That path previously fell
  back to the directory basename with no diagnostic, because the warning about failing
  to read the existing manifest sits inside the `command -v node` guard.

- The `project` value is escaped for JSON on write, as the quick context already was, so
  a quote or backslash in a preserved name or a directory basename cannot emit a
  `MANIFEST.json` that no longer parses.

- Committed handoff state and one gate comment no longer name private repositories. This
  repository is public and ships to npm, so naming consumer repositories here published
  a list of them to anyone reading the repository or the package. The `What is Missing`
  row keeps its measurements and now states them without identities (the figures
  themselves were removed on 2026-09-28, see CONSTITUTION.md). `.ai/handoff/` is outside the `files` list, so the row
  itself never reached the npm tarball, but `scripts/` is inside it, so the comment in
  `check-runtime-support.mjs` would have shipped on the next publish.

- A `no-private-repo-names` forbidden-pattern rule keeps most such names out. The same
  names had already been removed once, from the propagation playbook, and returned in a
  later change because nothing gated them. The rule matches on shape rather than on a
  list, so the gate does not itself have to enumerate the repositories it protects - but
  the shape it matches is one naming prefix, and not every name the row was built from
  carries it. One is a bare product word with no shared prefix, and nothing in its shape
  separates it from ordinary prose.
  Measured on this branch: a tracked file reintroducing that name leaves the gate
  reporting `Forbidden patterns OK: 2 rule(s), no matches.` and exiting 0, while the same
  file carrying a prefixed name exits 1. It was removed from the row by hand and stays
  out by review, not by the gate. Adding it to the rule is not the fix, because the rule
  lives in a tracked config file in this public repository and would then publish the one
  identity this change exists to withhold.

- The only CLI-level test of the prompt-injection detector no longer passes with the
  detector switched off. It appended an injection line to an indexed handoff file and
  asserted nothing but a non-zero exit; appending to an indexed file breaks its checksum,
  which is a different one of the seven lint checks and produces that same exit code, so
  the assertion was satisfied by the setup's side effect. Emptying `INJECTION_PATTERNS`
  left it reporting ok. It now regenerates `MANIFEST.json` after writing, so the tree is
  checksum-clean and the injection check is the only thing that can fail, and it asserts
  on the output. A negative control proves the same harness exits 0 on benign content.
  All ten patterns are now covered, each asserted BY NAME, so a typo in any one of them
  turns exactly that test red and says which; six of the ten had no coverage at all.

### Security
- The two required status checks that validate `MANIFEST.json` no longer download
  and execute unpinned third-party code. `lint-and-validate` and `aahp-manifest`
  ran `npm install --no-save ajv-cli ajv-formats` and executed the result on the
  next line, on every push and every pull request. `--no-save` guarantees the
  resolution is never written to `package-lock.json`, so 27 packages arrived with
  nothing in the repository constraining any of them, and one compromised release
  anywhere in that closure was arbitrary code execution inside the checks whose
  verdict certifies a change. `ajv-cli` (5.0.0) and `ajv-formats` (3.0.1) are now
  exact devDependencies locked by integrity hash, both jobs install that locked
  closure with `npm ci`, and both invoke the tool with `npx --no-install`. The
  `--no-install` is the load-bearing half: without it npx falls back to the
  registry whenever the local resolution misses, and the pin buys nothing.

- `npm run check:workflow-pinning` is why it stays that way. The root cause was
  not a forgotten pin, it was that pinning here was a hand-applied convention:
  the workflow template this package ships to consumers
  (`assets/governance/aahp-govern.yml`) has always used `npm ci --ignore-scripts`
  followed by `npx --no-install`, and the workflows that only ever ran here did
  not, because nothing made them. The gate asserts four properties over every
  workflow in `.github/workflows` and every template in `assets/governance`: no
  project-level `npm install`, every `npx` carries `--no-install`, every package a
  workflow executes is declared here at an exact version, and every direct
  dependency carries both `resolved` and `integrity` in the lockfile. A state it
  cannot evaluate exits 2 rather than 0, so "I could not look" never reads as
  "I looked and it was fine". Deliberately out of scope and named rather than
  quietly exempted: `npm install -g` in the publish job, a different risk class
  with an owner decision still open on it, tracked at
  https://github.com/homeofe/AAHP/issues/68.

- Three schema tests stop failing open. The two in `tests/handoff-impact.bats` and
  the one in `tests/init-gates.bats` skip when `ajv-cli` cannot be resolved, so
  they only ever ran in the one job that happened to have installed it and
  reported "# skip ajv-cli not installed" everywhere else while the check stayed
  green. Declaring the package means `npm ci` installs it and the tests execute.

- The version regex in the new gate is not exponentially ambiguous. Its first form
  repeated a group whose character class also contained the group's own delimiter,
  which CodeQL reported as `js/redos`. Measured on the input shape it named,
  `9.9.9+` followed by repeated `--`: 22 repetitions, a 51-character string, took
  11.8 seconds under the old form and 0.004 ms under the replacement. The input is
  a `package.json` version specifier, which is attacker-supplied on a fork pull
  request. What counts as an exact version did narrow, in exactly one place: the
  replacement's build class is `[0-9A-Za-z.]`, which drops the `-` the prerelease
  class still allows, so a SemVer-legal version whose build metadata contains a
  hyphen is now reported as a range. Measured old form against new,
  `1.0.0+21AF26D3----117B344092BD` (the SemVer specification's own example),
  `1.2.3+build-5`, `1.2.3-alpha+a-b`, `1.2.3-x+y-z` and `0.0.4+-` all went from
  accepted to rejected. The narrowing runs fail-closed - nothing unpinned became
  acceptable, a legal pin became unacceptable - and it is unreachable for the two
  packages this gate governs, both pinned without build metadata. It is still a
  false positive, and the finding it prints ("Declare an exact version") misnames
  the cause, so if it is ever hit the fix is to put the hyphen back in the build
  class, which stays linear because `+` cannot appear inside it.

- The portable governance workflow AAHP ships, `assets/governance/aahp-govern.yml`, now
  declares `permissions: contents: read` and sets `persist-credentials: false` on its
  checkout. This is the change with reach beyond this repository: `aahp init --gates`
  copies that file into a consumer repository and `npm pack` puts it in the published
  tarball, so until now every adopter received a workflow that inherited whatever
  `default_workflow_permissions` their repository was set to and left the job's
  `GITHUB_TOKEN` in `.git/config` for every later step to read, including everything
  `npm ci` and `aahp` execute. AAHP cannot see an adopter's repository visibility, their
  default, or an organization default that grants `write`, so the workflow now states
  what it needs instead of inheriting it. The header explains both lines in place.
  **Adopters who already scaffolded the file keep their old copy:** `aahp init --gates`
  skips a workflow that already exists. Re-run it with `--force`, or add the two lines
  by hand, to pick this up.

- The six repository workflows that had no top-level `permissions:` block now declare
  `contents: read` (`aahp-archive.yml`, `aahp-lint.yml`, `aahp-manifest.yml`,
  `aahp-pii-allowlist.yml`, `ci.yml`, `codeql.yml`), and ten more `actions/checkout`
  steps set `persist-credentials: false`: the nine in `.github/workflows/` plus the one
  in `assets/governance/aahp-govern.yml`, the template this package ships to consumers.
  Nine is the `.github/workflows`-only figure and leaves out that template, which is the
  file in this change with reach beyond this repository. Counted across both locations
  the repository holds eleven checkout steps; exactly one, in `aahp-verify.yml`, already
  set the flag, so after this change all eleven set it. The job-level elevations
  are unchanged and still override the top-level block: `publish` keeps
  `id-token: write` for OIDC trusted publishing, `release` keeps `contents: write` for
  `gh release create`, and the CodeQL `analyze` job keeps `security-events: write` for
  its SARIF upload.
  Stated plainly, because the change should not be read as fixing an exploitable defect
  in this repository: it is not. This repository is public, its
  `default_workflow_permissions` is `read`, every job runs on a throwaway hosted runner,
  and the owner is a user account with no organization layer, so a read-only token in a
  public repository's workspace grants nothing an anonymous clone does not. The value is
  that a declared block cannot be widened by a settings change with no diff to review,
  and that it is measurably narrower even today: an inheriting job here is granted
  `Packages: read` on top of `Contents` and `Metadata`, and a declaring job is not.

- `aahp.config.json` is validated against `schema/aahp-config.schema.json` before any
  gate is evaluated, by `aahp check`, by `aahp doctor` and by every gate that loads the
  config directly. Nothing validated it before, and `gateApplies` decides whether a gate
  runs from the PRESENCE of a config key, so a key misspelled by one letter was
  indistinguishable from a section that was never written: `forbiddenPatterns` typed as
  `forbiddenPaterns` turned a FAILING gate into `SKIP  not applicable here` and the run
  into `Governance OK`, exit 0, with the violation still in the tree. An unparseable
  config was worse - `readJsonSafe` returned null, the caller substituted `{}`, and all
  eight gates skipped. This is the gate over the gates: while the config could be
  malformed unnoticed, every gate it declares was optional in practice. An invalid
  config is now an error that names the offending key and suggests the closest valid
  one, no gate is evaluated, and the machine-readable record marks every gate
  `unevaluated` rather than `skip`, so a dashboard can tell "asked, not applicable here"
  from "never asked". The validator (`scripts/aahp-schema.mjs`) is dependency-free and
  ships inside the package, per ADR-002; it THROWS on any schema keyword it does not
  implement, so a partial validator can never report "valid" for a document it did not
  fully examine. CI validates this repository's own config with AJV as well, so the two
  implementations cross-check.

- No code AAHP ships can fetch a package from the public registry as a fallback. The
  portable governance workflow and both git hooks called `npx --no-install aahp`.
  `npx` is `npm exec`, which has NO `--no-install` option and ignores the unknown flag
  without a warning, so whenever the local resolution missed, that line resolved the
  UNSCOPED name `aahp` against registry.npmjs.org and executed whatever came back - as
  the repository's governance gate, and inside the pre-commit and pre-push hook of every
  consumer that installed them. The published package is `@elvatis_com/aahp`; the
  unscoped name is owned by nobody, so the fallback would not even have resolved to a
  stale version of this project. It returns 404 today, which is why the behaviour was
  correct BY ACCIDENT rather than by construction. All five call sites now invoke
  `node <root>/node_modules/@elvatis_com/aahp/bin/aahp.js`, a path that cannot reach the
  registry and cannot resolve to any name but the pinned one; when it is absent the
  workflow fails closed and the hooks skip, which is what `README` always said they did.
  **Adopters: there is no single command that fixes this for you, and the instruction
  this entry first carried did not reach a single repository.** It said to run
  `aahp init --gates --force`. That command writes exactly one file,
  `.github/workflows/aahp-govern.yml`. In a repository whose gate is
  `.github/workflows/aahp-verify.yml`, running it would have added a second,
  unreferenced workflow beside the one that actually gates the branch, and left the
  vulnerable line untouched in the file that runs. `scripts/install-hooks.sh` does not
  help where no hooks are vendored under `scripts/hooks/`: there is no installed copy
  for it to refresh.
  What those repositories actually contain is a hand-held `aahp-verify.yml` that AAHP
  does not generate, so AAHP cannot rewrite it. Fix it by editing that file yourself:

  - If the step reads `npx --no-install aahp verify . --level ci` (eight of the ten),
    it resolves the UNSCOPED name and is the vulnerable spelling. Replace `npx
    --no-install aahp` with `node node_modules/@elvatis_com/aahp/bin/aahp.js`, keeping
    the `npm ci` step that installs the exact-pinned devDependency ahead of it. Do the
    same for the `doctor` step beside it.
  - If the step reads `npx -y @elvatis_com/aahp@<version>` (two of the ten), it already
    names the scoped package at an exact version, so it cannot resolve to the unowned
    name and needs no change. It does fetch from the registry on every run by design;
    that is a deliberate invocation, not the silent fallback this entry is about.
  - If you have vendored hooks under `scripts/hooks/`, re-run `scripts/install-hooks.sh`.
    If you have a scaffolded `aahp-govern.yml`, `aahp init --gates --force` refreshes it.
    Neither applies to a repository that only consumes the published package.

  Fixing the sources here does not fix a copy, and no `init` command can reach a file it
  never wrote.

- Four of the thirteen secret patterns `aahp lint` enforces matched nothing at all.
  `grep` runs in BRE mode, where the bare `?` in `['\"]?` is a LITERAL question mark
  rather than an optional-quantifier, so `_KEY=`, `_SECRET=`, `_TOKEN=` and `_PASSWORD=`
  required a quote followed by an actual `?` character. Measured on a fixture containing
  `API_KEY=abc123`, `X_SECRET=...`, `GH_TOKEN=...` and `DB_PASSWORD=...`: zero matches
  before, one to two each after escaping the quantifier. The same trap was already
  documented one comment above for the `\{16,\}` interval and was applied there.
  `_CREDENTIALS=` is added alongside them, because `templates/.aiignore` has always
  shipped `*_CREDENTIALS=*` with no counterpart in the enforced list.
  **These five patterns carry the same length floor as the nine prefix patterns**, for
  the reason the comment above them has always given. Escaping the quantifier alone
  produced `_KEY=['\"]\?[a-zA-Z0-9]`, which matches a `*_KEY=` assignment of ANY value
  from one character upwards - a detector for the SHAPE of a configuration line, not for
  a secret, in files that are full of prose describing configuration. Measured before
  the floor was added: one consumer repository went from
  `All checks passed` exit 0 to `1 violation(s) found` exit 1 on a single committed line,
  and that line was a handoff note DESCRIBING a security finding - it quoted the
  placeholder `API_KEY=your-api-key-here` from an `.env.example` the note was arguing
  against. That repository's `aahp verify --level ci` gate is required and
  branch-protected, so upgrading alone would have turned a green protected branch red
  with nothing in the repository changed. On a twelve-line prose corpus the unfloored
  spelling scored eight false positives; with the floor it scores zero and still matches
  all eight entries of a real-secret corpus, including the segmented modern formats
  (`sk-proj-`, `github_pat_`, `rk_live_`) that a floor anchored at `=` would miss. In
  the handoff directories available to measure, no pattern fired either before or after,
  so no adopter was expected to change state on upgrade. What the floor deliberately gives up is
  a short real password such as `DB_PASSWORD=hunter2`: nothing distinguishes that from
  prose by inspection, and a repository that needs it should run a purpose-built entropy
  scanner. A secret finding now prints `path:line` rather than a bare filename, and never
  echoes the matched text - a real secret must not be republished into a CI log.

## [3.10.0] - 2026-08-20
**Fail-closed Layer 2 base selection and reviewed exact-file impact classification**

### Added

- `handoffImpact.nonImpactingModifiedFiles` provides an optional, reviewed Layer 2
  classification for maintenance-only files. Each entry requires one exact
  repo-relative regular tracked `file` and a reviewable `reason` containing a visible
  letter or number. Only a content-only modification (`M`) whose old and new regular-file
  modes match can be non-impacting; additions, deletions, renames, copies, type changes, and
  mixed source changes remain impacting. Every applied classification logs the file and
  reason.
- `aahp verify --base SHA` and its `AAHP_BASE_SHA` environment equivalent anchor the
  Layer 2 diff explicitly. The required workflow passes the pull request base SHA for a
  pull request, the event `before` SHA for a push, and a required input for a manual run.

### Changed

- The config schema, example, specification, architectural decision log, and rollout
  guidance now define the exact-file M-only contract and explicit CI base requirement.
- The required workflow declares read-only contents permission, disables persisted
  checkout credentials, and pins every third-party action to an immutable commit.
- Documentation now states the pull-request trust boundary explicitly: requiring the
  status is not sufficient unless repository rules also require trusted review for the
  workflow and the gate/parser paths it executes.
- Propagation coverage proves the workflow and shared parser travel together.
- The npm artifact now includes the canonical verify workflow consumed by
  `scripts/propagate.sh`; packed-artifact coverage installs the tarball and proves the
  propagated gate, parser, and workflow are byte-identical to the release sources.

### Fixed

- `--level ci` no longer guesses a base or permits a vacuous HEAD-versus-HEAD diff.
  Missing, all-zero, malformed, unreadable, HEAD-equal bases and any git diff failure
  are blocking findings rather than empty change sets that can report green. The gate
  compares base and HEAD endpoint trees so a rollback or force-push cannot collapse to
  an empty merge-base-to-HEAD diff.
- The required workflow no longer bypasses verification for an actor. Layer 1 now runs
  for every change, including automated dependency updates.
- The optional impact parser fails closed without relying on an external schema command:
  malformed JSON and types, non-standard numeric constants, empty or invisible reasons,
  control or format characters, absolute or traversal paths, globs and
  metacharacters, directories, untracked paths, symlinks, gitlinks, handoff paths,
  the config itself, duplicate JSON keys, duplicate entries, and prefix-like ambiguity
  are rejected. The policy file itself must be a regular tracked Git object; a symlink
  cannot delegate policy to a mutable referent. Untracked or unstaged working-tree policy
  cannot authorize an index change, and mode-only or content-plus-mode changes cannot use
  the content-only exception.

## [3.9.2] - 2026-08-05
**Windows bash portability, one resolver; MANIFEST project-name preservation**

### Changed

- Bash interpreter resolution and Windows path conversion now have a single implementation,
  `resolveBash()` and `toBashPath()` in `aahp-config.mjs`, used by both `bin/aahp.js` and
  `scripts/aahp-dashboard.mjs`. `bin/aahp.js` previously carried its own
  `findBashExecutable()`/`toBashScriptArg()` pair; the dashboard call site had neither, so
  the same Windows defect had to be found a second time. The merged helper keeps what each
  side got right: the relative-path and `/c/` MSYS strategies from the CLI, and the
  `AAHP_BASH` override plus environment-driven candidate paths (including Git's `usr/bin`
  layout and per-user `LOCALAPPDATA` installs) from the new one. `platform`, `env` and `cwd`
  are injectable, so the win32 behaviour is asserted on the Linux CI runner
  (`tests/bash-portability.bats`), and a test fails if a second copy is reintroduced.

### Fixed

- `handoff-refresh` no longer fails on Windows when it regenerates `MANIFEST.json`.
  `aahp-dashboard.mjs` shelled out to a bare `bash` with native backslash paths, which
  breaks in two independent ways: bash consumes each backslash as an escape, and a bare
  `bash` on Windows can resolve to `C:\Windows\System32\bash.exe` (the WSL launcher),
  whose filesystem has no `C:` drive. `LOG.md` is written before the regen, so the failure
  left the manifest checksums stale against the file just produced. Only a project that
  configures `generate.log` reaches this code path, and only in write mode (`--check` exits
  before it), which is why AAHP's own dogfooding never hit it.
- `aahp-manifest.sh` no longer overwrites an existing MANIFEST.json `project` value with
  the checkout's directory basename on regeneration. Only a first-ever generation (no
  `MANIFEST.json` on disk yet) derives `project` from the basename; every regeneration
  after that preserves the recorded name. Previously, regenerating inside a
  differently-named checkout (a temp dir, a CI working directory, a tarball extraction)
  silently clobbered a consumer's real project name. Regression coverage in
  `tests/manifest.bats`.
- The same fix corrected a latent misalignment bug in the tasks/`next_task_id`/
  `cross_repo_ref` preservation added in 3.9.1: the single-node-process read joined
  fields with a tab and split them with `IFS=$'\t' read`, but tab is IFS whitespace, so
  bash silently collapses and strips leading empty fields. Any manifest with an empty
  earlier field and a non-empty later one (e.g. no `tasks` but a `cross_repo_ref`) had its
  fields shifted into the wrong variables. The delimiter is now `\x1f` (Unit Separator),
  which is not IFS whitespace.

## [3.9.1] - 2026-08-03
**Doctor handoff-set matches Layer 1 on partial indexes; handoff hygiene**

### Changed

- Dogfooded handoff hygiene: STATUS.md rewritten to a current-state snapshot (no session
  append log), WORKFLOW.md aligned with Phase 4.5 + MANIFEST task selection + harness-owned
  model routing, NEXT_ACTIONS.md single Recently Completed section, TRUST.md re-verified.
- Template `WORKFLOW.md` task-selection rules now point at MANIFEST.json as authority
  (DASHBOARD is display-only), matching README Section 8.4.

### Fixed

- `aahp doctor` `handoff-set` gate now fails when a canonical handoff file is present on
  disk but missing from `MANIFEST.json` `files{}` (partial index). Previously doctor could
  report PASS while `aahp verify` Layer 1 / `lint-handoff.sh` already failed the same state.
  Regression coverage in `tests/doctor.bats`.
- `aahp_auto_summary` looks past title/blockquote/header chrome (first 40 content lines)
  so regenerating a manifest no longer collapses every summary to `(no summary available)`
  on normal handoff files.

## [3.9.0] - 2026-07-26
**Acceptance-criteria lifecycle, plus an advisory report that is deliberately not a gate**

### Added

- Specification Section 8.7 defines the acceptance-criteria lifecycle: one canonical
  `Acceptance criteria` section per task, `- [ ]` while a criterion is unresolved, `- [x]`
  only on evidence, and, before a task becomes `done` or a linked issue closes, every
  remaining criterion completed, explicitly waived (`(waived: rationale)`), or moved to a
  linked open follow-up (`(follow-up: T-042)`). `Completion criteria` and
  `Definition of done` stay recognized as legacy aliases with a documented rename path.
- `aahp criteria [path]`, an ADVISORY report over the lifecycle, backed by
  `scripts/report-acceptance-criteria.mjs`. It is not part of `aahp check`, it has no
  enforcing mode, and it always exits 0 whatever it finds; the known non-zero exits are the
  report failing to run at all (an unparseable config, or no git work tree). Best effort
  by construction, which is why it is advisory and gates nothing. It reports
  three lifecycle defects (`legacy-heading`, `plain-bullets`, `unresolved-on-done`) and
  ten comprehension defects so that input it could not read is never presented as a
  clean document: `config-unusable`, `include-unusable`, `no-files-matched`,
  `file-unreadable`, `manifest-missing`, `manifest-outside-root`, `manifest-unreadable`,
  `unparsed-criteria-section`, `unbound-criteria-section`, and `unterminated-fence`. It
  reads tracked files plus the `MANIFEST.json` task registry and makes no network calls,
  so a run is complete and deterministic offline.
- The always-exits-0 guarantee holds for every input the report accepts. A syntactically
  valid `acceptanceCriteria.include` pathspec that git nonetheless refuses (an unknown
  pathspec magic word, a path outside the repository) is reported as `include-unusable`
  rather than throwing out of the process, so the documented pair of non-zero exits is the
  complete list. The same holds at every level of the configuration: an `aahp.config.json`
  that parses but is not a JSON object (`null`, a string, a number, a boolean, an array) is
  reported as `config-unusable` and the report continues on its defaults. A file containing
  only `null` previously threw a `TypeError` out of the process on a config that parses
  perfectly well, and the other non-object shapes read as no configuration at all without
  saying so. The same shapes are covered for `acceptanceCriteria`, for
  `acceptanceCriteria.include`, and for `acceptanceCriteria.manifest`.
- Every configuration finding names `aahp.config.json`, the only file `loadConfig` reads.
  When no config file existed they named `package.json`, which the loader never opens, so a
  reader was sent to correct `acceptanceCriteria.include` in the wrong file.
- `acceptanceCriteria.manifest` is required to resolve inside the project root. A value
  containing `..` used to be joined to the root and read from outside the work tree; it is
  now reported as `manifest-outside-root` and the file is never opened.
- README Section 8.7 publishes the report's known blind spots by name, starting with the
  most reachable one: the criteria heading must match a recognized phrase exactly after
  normalization, so `## Acceptance criteria for release` opens no section and everything
  under it is missed in silence. The list also covers the case where a bold line inside a
  criteria section ends the section and hides every criterion after it. It states in plain
  words that the report is best effort, that a clean report is not proof the criteria are
  resolved, and that it must not be used as a merge gate.
- Task ids bind from ATX headings, setext headings, and bold labels, so the three heading
  forms that appear in hand-written handoff files all scope a criteria section. Anything
  still unattributable is reported as `unbound-criteria-section` instead of being exempted
  from the done-state rule in silence.
- `acceptanceCriteria` (`include` / `manifest`) in `schema/aahp-config.schema.json` and
  `aahp.config.example.json`. It supplies the report's input paths only; it configures no
  gate and it cannot enable one.
- Acceptance-criteria task boxes in both `.github/ISSUE_TEMPLATE` files, the lifecycle rule
  in `templates/CONVENTIONS.md`, and `tests/acceptance-criteria.bats` covering canonical,
  legacy-heading, plain-bullet, waived, follow-up, invalid-closure, and offline cases, the
  comprehension defects above, the exit-0-with-findings guarantee, the unchanged `aahp
  check` gate set, and the published blind spots as explicit known-limitation tests.
- ADR-017: a heuristic over hand-written prose is a report, never a gate. It records the
  three adversarial review rounds that produced the decision and why an opt-in enforcing
  mode is not a sufficient safeguard.

### Changed

- `aahp check` is untouched by this release. The acceptance-criteria detection is a
  standalone command, so the gate list, the `aahp check --json` record shape and its exit
  code are exactly what they were in 3.8.2: the same eight gate ids with the same statuses,
  verified by running both the 3.8.2 and the 3.9.0 CLI against the same tree.
- This repository's own handoff is conformant under the report. `MANIFEST.json` task keys
  now match the ids used in `NEXT_ACTIONS.md` (the two entries whose titles carried a
  different id in brackets were re-keyed), and the criteria of the completed tasks are
  resolved individually: checked with the evidence named inline, or left unchecked with a
  stated waiver where the criterion was superseded. The npm publish task is recorded `done`
  with the registry as its evidence, and its stale "blocked on human npm auth" note is
  removed; its second criterion is waived because the package shipped under a scoped name,
  so the bare `npx aahp` form the criterion names cannot resolve to it.
- `templates/NEXT_ACTIONS.md` uses the canonical `Acceptance criteria` heading instead of
  `Definition of done`, and states the lifecycle rule in its header.
- The shipped scaffolding is self-consistent. `templates/MANIFEST.json` marked the example
  `T-001` as `done` while `templates/NEXT_ACTIONS.md` carried unchecked criteria for it.
  `T-001` is now `in_progress`, which is what the template document actually shows.

### Removed

- The `acceptanceCriteria.strict` config key, and with it every way to make an
  acceptance-criteria finding fail a build. It never shipped in a release. An enforcing
  option would be switched on somewhere, and then a document shape nobody anticipated
  becomes a red build in a consumer repo, so the option does not exist rather than
  defaulting to off. See ADR-017.

### Fixed

- Criteria written as an ordered list (`1.`, `2)`) are recognized. They were invisible to
  every rule, so a task marked `done` with unresolved numbered criteria reported completely
  clean. Both list forms now count, and README Section 8.7 states which forms are criteria
  and which are not.
- Fenced code blocks are skipped. The parser carried no fence state, so documentation that
  SHOWS an example of the criteria format was read as criteria that exist, which fired
  hardest on the projects most likely to document the convention.
- A `MANIFEST.json` that is present but unparseable is reported as `manifest-unreadable`
  instead of being treated as an absent one, and a task registry path that was configured
  explicitly but does not exist is reported as `manifest-missing`. Both used to disable the
  `unresolved-on-done` rule in silence.
- A config value of the wrong shape is reported as `config-unusable` instead of being
  replaced by a default without a word, and a tracked file that cannot be read is reported
  as `file-unreadable` instead of being skipped silently.
- Task scope survives an intervening heading. Any non-task heading between a task heading
  and its criteria heading used to reset scope to null, so `unresolved-on-done` could never
  fire for a task whose document puts a `### Context` or `### Files` subsection in between.
  Scope now closes only on a sibling or ancestor heading.
- `scripts/report-acceptance-criteria.mjs` has no import-time side effects. It runs only
  when the module is the process entry point, so `parseCriteriaSections` and
  `findSectionDefects` can be imported without it reading the filesystem, printing, or
  calling `process.exit`.
- `package-lock.json` is regenerated so its root name and version match `package.json`
  (it had drifted to the pre-scope name at `3.5.0`).

## [3.8.3] - 2026-07-26

### Changed

- Consequence of the exit-code fix, worth knowing before upgrading: `aahp lint` now exits 1
  on a repository that has run `aahp init` but not yet `aahp manifest`, because the
  scaffolded manifest still carries the template placeholder `sha256:[hash]` for every
  file. `aahp verify` has always refused that state, so no blocking verdict changes; only
  lint stops disagreeing with what it prints. Run `aahp manifest` after `aahp init`.
- `aahp verify` Layer 1 reaches BOTH integrity verdicts itself: it reads the file index out
  of `MANIFEST.json` and hashes each indexed file, instead of inferring existence or a
  mismatch from another script's output. Blocking no longer rests on string-matching
  between two scripts, and it survives `lint-handoff.sh` being unavailable, changing its
  wording, or dying before it prints anything. `lint-handoff.sh` still runs for the checks
  Layer 1 does not cover, and its non-zero exit is still honoured.
- `scripts/lint-handoff.sh` no longer ends with "All checks passed" when it skipped its
  integrity check because no Python interpreter is available. That single case stays a
  warning with exit 0 on purpose, since making it a violation would turn currently green
  node-only environments red without catching anything `aahp verify` Layer 1 does not
  already catch; the summary now says that MANIFEST integrity was not verified, and the
  README documents the exception instead of claiming that every unverifiable state exits
  1.

### Fixed

- `aahp verify` Layer 1 now fails when `MANIFEST.json` indexes a file that is not present
  in the working tree. Deleting an indexed handoff file used to pass both `aahp lint` and
  `aahp verify --level ci` silently: the checksum comparison answers "does this file still
  match what the manifest recorded", and a deleted file has no content to mismatch, so the
  comparison never fired for it. The manifest could therefore keep advertising an artefact
  that no longer existed while the blocking gate stayed green. A missing indexed file is
  now reported by name and with its own message, separately from a checksum mismatch,
  because the two need different fixes: restore the file, or regenerate the manifest.
- `aahp doctor`'s `handoff-set` gate already caught this case, so the two gates disagreed
  about the same repository state. They now agree.
- `scripts/lint-handoff.sh` raises its own exit code for a failed integrity check. It
  previously printed `! Checksum mismatch` and still exited 0, and still printed
  "All checks passed", because the comparison runs in an embedded interpreter that cannot
  write to the calling shell's violation counter. Two CI workflows and the documented exit
  contract already trusted that exit code, so a hook or a job wired to it got a pass on a
  corrupted handoff set. Both integrity failures now count as violations.
- `scripts/lint-handoff.sh` no longer converts an unexpected exit code from the checksum
  verifier into a yellow note. Any exit code other than "clean" or "findings" means the
  tool could not establish integrity, and unproven integrity is now a violation. Before
  this, an interpreter that died inside the loop produced "All checks passed" and exit 0
  over a tampered handoff set, which is the same fail-open the rest of this release cures.
- `scripts/verify-handoff.sh` guards the shared library helper it depends on. Under
  `set -euo pipefail` an absent helper aborted the gate at exit 127 with no diagnostic;
  a partially synced repository now gets a message that names the missing helper and says
  the library is out of date.
- A `MANIFEST.json` whose `files` index is empty is now a finding in both scripts. Zero
  indexed files means zero comparisons ran, which is not the same as everything matching.
- The manifest-reading helper no longer turns its own failures into "nothing is missing".
  A manifest that is absent, unreadable, or unparseable, and the case where neither node
  nor python is available, are each reported with a distinct exit code, and Layer 1 fails
  on all of them rather than printing an affirmative pass.
- A PARTIAL `files` index is now a finding in both scripts, exactly like an empty one.
  Removing one entry and rewriting that file used to pass both gates: every remaining
  entry still matched, and the file that changed had nothing to be compared against. The
  missing-file check could not see it either, because the file is present. Both scripts
  now fail when a canonical handoff file exists in `.ai/handoff/` and has no entry in
  `files`, which is the invariant the manifest generator already produces.
- The python fallback in the manifest-reading helper wrote the file index through
  text-mode stdout, so on Windows every line came back CRLF. The trailing carriage return
  was carried into the recorded checksum and every comparison mismatched, which would have
  reported a false integrity failure in every repository on any machine that takes that
  fallback (that is, any machine without node). The helper now writes bytes, and the
  reader additionally strips a trailing carriage return so a stale emitter cannot bring
  the false mismatch back.
- A deleted `.ai/handoff/MANIFEST.json` is a violation in `scripts/lint-handoff.sh`
  instead of a yellow note. With no manifest there is no index, so not one handoff file
  was compared, which is the maximal unproven state; lint nevertheless printed
  "All checks passed" and exited 0, and the `aahp-lint` workflow job runs that exit code
  as its own blocking check. `aahp verify` Layer 1 already failed here, so the two gates
  now agree.
- `aahp_checksum` returns non-zero instead of succeeding with an empty digest. When the
  checksum tool produced no output the function reported success with `sha256:` and
  nothing after it, so the branch written for "could not compute a checksum" was
  unreachable and the operator was sent to the wrong fix: regenerating the manifest baked
  the empty digest in, after which a broken toolchain reported a clean handoff set.

## [3.8.2] - 2026-07-25

### Changed

- Applicability of the version-derived gates is decided on the PRESENCE of a root
  `package.json`, not on it parsing. A `package.json` that exists but is not valid JSON no
  longer makes `changelog`, `changelog-format` and `version-sync` skip silently in
  `aahp check`; the gates run and fail with the parse error, so a broken manifest is loud
  rather than invisible.

### Fixed

- Gate applicability on a project root with no `package.json` is now decided by a single
  predicate shared by `aahp doctor` and `aahp check`, so the two commands can no longer
  hold different opinions about the same repository. A polyglot root (for example a Python
  service whose only `package.json` lives in a frontend subdirectory) with a valid
  `.ai/handoff/` set and a `CHANGELOG.md` used to be green under `aahp verify` but red
  under both diagnostics: `doctor` reported `changelog-format = fail` because the gate
  loaded the root `package.json` for a version before it ever opened the changelog, and
  `check` reported `handoff = fail` with "package.json not found". A gate that cannot
  apply now skips instead of failing.
- `scripts/aahp-dashboard.mjs` treats the root `package.json` as optional. Without one it
  skips the `NEXT_ACTIONS` current-version comparison (there is no version to compare
  against) and falls back to the root directory name for a generated LOG title, instead of
  exiting 1 before doing any work. A `package.json` that is present but malformed still
  fails loudly.
- `aahp doctor` also skips `version-sync` and `pinned-dep` on a root with no
  `package.json`, for the same reason: neither gate has anything to check against.

## [3.8.1] - 2026-07-19

### Fixed

- `scripts/aahp-manifest.sh` now passes the manifest path to its Node helpers as an
  argument (`process.argv[1]`) instead of interpolating `$HANDOFF_DIR` into the inline
  script. On Windows and MSYS checkouts the interpolated path was not readable by native
  Node, so the preservation helpers failed silently and `tasks`, `next_task_id`, and
  `cross_repo_ref` were dropped from MANIFEST.json on regeneration. Linux and CI were
  unaffected. Regeneration now preserves those optional fields on every platform.

## [3.8.0] - 2026-07-18
**Portable Governance: one aggregate governance gate, a governance-only conformance record, and a drop-in CI workflow**

### Added

- `aahp check [path]`: a consumer-facing governance aggregator that runs the
  config-driven gates (changelog, changelog-format, version-sync, claims,
  forbidden-patterns, schema-doc-sync, doc-links, handoff) as one run. Each gate reports
  pass, fail, or skip, and the exit code is 0 only when no gate fails (a skipped gate
  never fails). `--json` emits a `schemaVersion: 1` record; `--quiet` prints only
  failures; `config.check.only` / `config.check.skip` select which gates run.
- `aahp doctor --governance` (alias `--no-handoff`): a governance-only conformance record
  that forces the three handoff gates to `skip` without evaluating them, so a repo with no
  `.ai/handoff/` can still emit a green record. The default mode is unchanged and
  byte-identical to prior versions.
- `aahp init --gates`: scaffolds governance-only config without creating `.ai/handoff/`
  (writes `aahp.config.json`, adds a `govern` script to an existing `package.json`, and
  writes `.github/workflows/aahp-govern.yml`).
- An opt-in, config-driven pinned-dep gate for `aahp doctor`: `pinnedDep`
  (`name` / `location` / `allowRange`) asserts the distribution pin. Absent config reports
  `skip`; the defaults reproduce the prior exact-pin behavior; a repo whose own package
  name matches reports `self`.
- `assets/governance/aahp-govern.yml`: a portable, opt-in, verify-only governance workflow
  that invokes `aahp check` and `aahp doctor --governance` through the pinned
  devDependency via `npx --no-install` (no vendored script paths).
- New `check` (`only` / `skip`) and `pinnedDep` (`name` / `location` / `allowRange`) keys
  in `schema/aahp-config.schema.json` and `aahp.config.example.json`.
- README: ADR-011 through ADR-016 in Section 7, plus `aahp check` and
  `aahp doctor --governance` coverage in Sections 2.11 and 9.2.

### Changed

- Git hooks are de-vendored: they resolve `scripts/verify-handoff.sh` when it is vendored,
  else the installed `aahp` CLI via `npx --no-install`, and skip when neither resolves. The
  required CI check remains the off-machine authority (the evaluator-path trust boundary
  is clarified in 3.10.0).
- The enumerating governance gates (`check-forbidden-patterns.mjs`, `check-doc-links.mjs`)
  fail loud outside a git work tree instead of silently scanning zero files: file
  enumeration goes through a shared `git ls-files` helper that throws when the project root
  is not a git checkout.

## [3.7.0] - 2026-07-18
**Anti-entropy: enforcement gates, a constitution, and an ADR log**

### Added

- Three config-driven enforcement gates (a clean no-op without config), folded into
  `npm run check`: `check-forbidden-patterns.mjs` (bans configured regexes such as em
  dashes or model names in tracked files), `check-schema-doc-sync.mjs` (asserts an
  extracted value-set is identical across sources, e.g. an enum in the schema vs its
  doc copies), and `check-doc-links.mjs` (resolves internal Markdown file links). Their
  config keys (`forbiddenPatterns`, `docSync`, `docLinks`) are documented in
  `schema/aahp-config.schema.json` and `aahp.config.example.json`.
- `CONSTITUTION.md`: a short, stable index of the project's non-negotiable invariants
  (each already enforced by a gate, test, or CI), linked from the README and CLAUDE.md.
- README Section 7 reframed as an Architectural Decision Log: 10 ADRs with stable
  `ADR-NNN` anchors and a LOG-to-ADR promotion rule.

### Changed

- The ci.yml ShellCheck step uses a `git ls-files` glob instead of a hand-maintained
  list, so new scripts are covered automatically (`scripts/propagate.sh` had been
  missed).
- Removed the stale German release runbook from the CONVENTIONS template and the
  dogfood, and collapsed the duplicated Three Laws motto to a single home (README).

## [3.6.1] - 2026-07-18
**Security: harden the claims floorCmd; fix shipped documentation drift**

### Fixed

- Documentation drift: README Section 4 now lists the full canonical handoff set
  (adds `GROUNDING.md`, `pii-allowlist.json`, `LOG-ARCHIVE.index.json`); the
  Section 7.1 command table adds `aahp doctor`; the Section 8.3 task-status enum
  adds `cancelled`; and the phantom `stale` bucket (never in the schema) is
  removed from `aahp status`.
- Removed em dashes (U+2014) from the CONVENTIONS templates and CLAUDE.md.

### Security

- `check-claims.mjs` now runs `floorCmd` as a repo-relative Node script via
  `execFileSync` (no shell), instead of `execSync` on an arbitrary config string.
  This closes a command-injection path from a PR-editable `aahp.config.json`
  (a contributor could otherwise gain code execution in a consumer's CI). The
  schema and example are updated to match; a path escaping the project is rejected,
  including Windows cross-drive and absolute paths.
- Hardened `aahp status`: the task-status counter uses a null-prototype object so a
  crafted status like `toString` cannot match via the prototype chain.

## [3.6.0] - 2026-07-18

### Added

- `aahp doctor`: a conformance self-check that emits a machine-readable JSON record
  (`schemaVersion: 1`) covering the handoff file set, MANIFEST schema conformance,
  GROUNDING/TRUST provenance, an exact-version dependency pin, changelog format, and
  version sync. `--json` prints the record to stdout.
- Config-driven release gates that ship in the package and run against any consumer
  project via `aahp.config.json`: `check-version-sync.mjs`, `check-changelog.mjs`,
  `check-changelog-format.mjs`, and `check-claims.mjs`. Each is a no-op until a repo opts
  in, so projects without config keep working.
- A single shared changelog grammar (`changelog-grammar.mjs`) imported by both the format
  validator and the optional LOG release-journal generator (`aahp-dashboard.mjs`), so the
  two cannot diverge.
- `schema/aahp-config.schema.json` and `aahp.config.example.json` documenting the config
  shape (`versionSites`, `claims`, `generate`), plus a `NEXT_ACTIONS.md` current-version
  freshness gate.
- README Section 2.11 (conformance and the config-driven gates) and a documented release
  ceremony. A `Provenance` column was adopted in the dogfooded `.ai/handoff/TRUST.md`.

### Changed

- `check-changelog-format.mjs` enforces the Keep a Changelog grammar (R1-R8, with
  `## [Unreleased]` optional); this CHANGELOG was normalized to conform (gave `3.1.0` a
  full ISO date and collapsed the `3.0.1-3.0.5` range into a single dated entry).

## [3.5.0] - 2026-07-14

### Added

- `documentation` pipeline phase, accepted by `aahp manifest --phase documentation`, the
  schema `last_session.phase` enum, the manifest generator, and the CLI help.
- This CHANGELOG. The GitHub release job references it.

### Fixed

- `aahp-manifest.sh` now preserves the optional `cross_repo_ref` field across
  regeneration (previously it was dropped), the same way it preserves `project`,
  `tasks`, and `next_task_id`.

## [3.4.0] - 2026-07-14

### Added

- README Section 9, Consuming Harness Integration: the harness-vs-AAHP boundary and
  decision matrix, a reference Claude Code `.claude/` layout, the minimal harness
  bootstrap, and grounding-audit integration.
- README Section 10, Multi-Repo and Cross-Repo Handoff, including the optional additive
  `cross_repo_ref` MANIFEST field (`repo` / `commit` / `handoff_file` / `relation`) and
  its schema entry, plus monorepo and version-skew doctrine.
- `aahp status` command documentation and an eight-command CLI reference table (7.1).
- A condensed inline Grounding reference in Section 2.10 (task-type anchor matrix,
  confidence bands, minimum TRUST fields).

## [3.3.0] - 2026-07-13

### Added

- Grounded Reflection Layer (Draft v0.1, README Section 2.10): an orthogonal provenance
  axis (`model_claim` to `human_confirmed`) recorded as a TRUST.md column, a
  `templates/GROUNDING.md` task-type anchor matrix scaffolded by `aahp init`, an optional
  pre-handoff Phase 4.5 grounding audit, and the `aahp migrate-grounding` verb. Additive
  and backward compatible: no MANIFEST field or schema change.

## [3.2.1] - 2026-06-26

### Fixed

- Follow-up fixes to the LOG archive flow and the verify gate. Last version published to
  npm before the 3.5.0 catch-up release.

## [3.2.0] - 2026-06-26

### Added

- Canonical LOG archive flow: `LOG.md` keeps the 10 newest entries and older entries
  rotate into `LOG-ARCHIVE.md`; `LOG-ARCHIVE.index.json` records archived-entry hashes so
  `aahp archive --verify` detects truncation or tampering. Reusable per-check badge
  workflows were split out for downstream repos.

## [3.1.0] - 2026-06-26

### Added

- Reviewed, exact-value, expiring PII email allowlist (`pii-allowlist.json`),
  MANIFEST-indexed so it cannot suppress secrets. First published to npm in 3.2.1
  (3.2.0 was tagged but never published).

## [3.0.5] - 2026-06-20

### Added

- AAHP v3 (v3.0.1 through v3.0.5): stable task IDs (`T-001` and up), a machine-readable
  dependency graph in `MANIFEST.json`, the `aahp` CLI, the verify gate (`aahp verify`),
  checksum integrity, the prompt-injection and secrets/PII firewalls, and OIDC trusted
  publishing to npm.

### Changed

- Relicensed to Apache-2.0 (earlier commits carried MIT, then CC BY 4.0, headers).

[Unreleased]: https://github.com/homeofe/AAHP/compare/v4.0.0...HEAD
[4.0.0]: https://github.com/homeofe/AAHP/compare/v3.12.0...v4.0.0
[3.12.0]: https://github.com/homeofe/AAHP/compare/v3.11.0...v3.12.0
[3.11.0]: https://github.com/homeofe/AAHP/compare/v3.10.0...v3.11.0
[3.10.0]: https://github.com/homeofe/AAHP/compare/v3.9.2...v3.10.0
[3.9.2]: https://github.com/homeofe/AAHP/compare/v3.9.1...v3.9.2
[3.9.1]: https://github.com/homeofe/AAHP/compare/v3.9.0...v3.9.1
[3.9.0]: https://github.com/homeofe/AAHP/compare/v3.8.1...v3.9.0
[3.8.3]: https://github.com/homeofe/AAHP/commit/038939ba7e86dbe268b5305444b7352b233eb23c
[3.8.2]: https://github.com/homeofe/AAHP/commit/7b6e543a3e8a98968b8905de666c740cc5ef1149
[3.8.1]: https://github.com/homeofe/AAHP/compare/v3.8.0...v3.8.1
[3.8.0]: https://github.com/homeofe/AAHP/compare/v3.7.0...v3.8.0
[3.7.0]: https://github.com/homeofe/AAHP/compare/v3.6.1...v3.7.0
[3.6.1]: https://github.com/homeofe/AAHP/compare/v3.6.0...v3.6.1
[3.6.0]: https://github.com/homeofe/AAHP/compare/v3.5.0...v3.6.0
[3.5.0]: https://github.com/homeofe/AAHP/releases/tag/v3.5.0
[3.4.0]: https://github.com/homeofe/AAHP/commit/b98d269d7f8d4bb9424c09e9135455ba2e9dcb47
[3.3.0]: https://github.com/homeofe/AAHP/commit/f040bfbc4d049cd4b0a8e1923196ef6f1e75b9c3
[3.2.1]: https://github.com/homeofe/AAHP/releases/tag/v3.2.1
[3.2.0]: https://github.com/homeofe/AAHP/releases/tag/v3.2.0
[3.1.0]: https://github.com/homeofe/AAHP/commit/0aff4206a296798afbe37e60a3e156d243a3356d
[3.0.5]: https://github.com/homeofe/AAHP/releases/tag/v3.0.5

