# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.20.5] - 2026-11-11

### Fixed — a scratch write offered a decision register for the wrong directory
The backfill offer says "this project has no decision register, and this run is changing
code in it". It fired after a write to a scratch file under `/tmp` from a session whose
workspace is a non-repo home directory: the file belonged to no project, the root fell
back to the session workspace, and the agent was told to "adopt" five ADRs from an
unrelated checkout that happened to live under `$HOME`. The offer now requires the
written file to live inside the project it describes, and a write outside it no longer
spends the run's one offer — a real source write later in the run still gets it.
`test-backfill.mjs` gains both checks (65/65); removing the guard, or moving it after the
offer is spent, each fails one.

## [0.20.4] - 2026-11-11

### Fixed — project artifacts resolved against the session workspace, not the project
A completion gate that cannot be satisfied is the worst kind of gate, and this one was
unreachable: `validateTrace` resolved a spec's `testFile` against `runRoot()` — the
directory pi happened to start in — so any session opened above the code it was changing
looked for `<parent>/test/x.mjs`, never found it, and answered "spec(s) marked met this
run without a trace" forever, however complete the trace was. The agent kept adding the
trace; the gate kept refusing it. That was one symptom of a family, and the family is
fixed here: the project's own `.asf/config.json`, Gate 5's `PLAN.md` and the config
screen all read the session workspace too, so a session started in a parent checkout read
the wrong config and refused a plan that existed. All of them now resolve through
`measureRoot()`/`measureRootSync()` (ADR-0005, rewritten to state the rule for the whole
family); run state deliberately stays at `runRoot()`.

The reason this shipped at all is that the trace check had no test. `test-trace-gate.mjs`
(15 checks) now drives the gate through real run state and makes the wrong-root case a
first-class check rather than an afterthought, and `test-measure-root.mjs` gained the
config, plan and config-screen consumers. Six mutations caught, including pinning
`measureRootSync()` back to the session root and putting Gate 5 back on `runRoot()`.

### Fixed — the spec store is redirected with everything else
`specTaskFileFor` now honours `ASF_STATE_ROOT`. It had no override, so the only way to
test the trace gate was to write specs into the developer's real `~/.pi` spec memory —
which is what the suite had been doing, in direct violation of ADR-0002. The fixture now
comes from ASF's own locator and a check asserts it cannot land under `~/.pi`.

## [0.20.3] - 2026-11-11

### Fixed — an open run could be erased by a session that never had it
The 0.20.2 fix made a close survive a stale writer. The opposite direction was still open,
and it is the half that loses work: a session that loaded the state file *before* a run
existed holds no `current`, and its next save wrote that emptiness over the open run for
everybody else. Only the in-process mirror in the session that opened the run could put it
back — so a run survived exactly as long as the session that started it, and a project
left with an open run in one process and none on disk. `mergeMonotonic` now treats
openness as monotonic too: a writer may clear `current` by closing a run (the tombstone),
never by having never known about it. `/asf abort`, the settle gate and `asf_run_complete`
all close through `archiveRun`, so a deliberate close still clears.

`mergeMonotonic` had reached cyclomatic complexity 25 while acquiring these rules; the
monotonic decisions and the gate's field merge are now named helpers (`resolveCurrent`,
`mergeRunFields`) rather than one long function, and the health gate is back to 0 violations.

## [0.20.2] - 2026-11-11

### Fixed — a closed run could be opened again
Two sessions on one project key left the same run archived to `history` twice and still
present in `current`: the run had been settled, resurrected, and settled again, so the
work that followed was judged against a run the completion gate had already closed. A
close is now a monotonic fact. `archiveRun` tombstones the closed `runId`; `mergeMonotonic`
refuses to write a tombstoned run back as `current`, instead of returning the incoming
copy wholesale whenever one side had no run; and `restoreFromMirror` restores a run only
when nothing recorded its close — which is what separates state that vanished outside ASF
(from the mirror's protection) from a run the gate just settled (which stays closed).
The tombstone list is bounded on both the write and the merge path.

The mirror's original job is untouched: a run deleted outside ASF is still restored, with
the disappearance recorded and the steer that says so — there is simply no tombstone for
it. Two new suites make the interleaving a test rather than an incident report.

## [0.20.1] - 2026-11-11

### Fixed — one run could be archived to history twice
Two sessions writing one project's state produced the same finished run twice in
`history`, minutes apart, and nothing in an entry said which run it belonged to — so the
record no longer guaranteed what it exists to guarantee, that the next run is judged
separately. Two causes, both fixed: `mergeMonotonic` merged `current` monotonically but
took `history` wholesale from whichever side wrote last, and the history append had no
notion of run identity. Run history is now written only through `archiveRun`, which keys
each entry by `runId` (falling back to its start for older records), refuses a second
entry for the same run, and reports `duplicate` in the `run-close` evidence so the
condition is visible rather than merely survivable. A merge now unions both sides by run
identity, oldest-first, so neither writer can erase or repeat the other's.

`/asf phase none` was also pushing its own history entry with a different shape and no
`runId`, which made ADR-0001's claim that `closeRun` is the only history writer false in
the code. It now goes through the same single writer and records `by: "phase-none"`, so a
close outside the settle gate is attributable.

## [0.20.0] - 2026-11-10

### Changed — the always-read budget is now the model's context window, not a byte ceiling
The always-read skill set was held to fixed byte ceilings. A byte ceiling can only ever be
met by splitting a rule in half, which weakens the rule to satisfy a number nobody chose,
so the budget is now a share of the model's context window: `session_start` reads
`ctx.getModel().contextWindow`, and an over-share set produces one non-blocking notice
naming the files and the tokens, never a refusal to load them. A model with no usable
context window is judged against the smallest supported window, so an unknown model makes
the check stricter, never switches it off. Token estimates round up — an estimate must not
flatter the set it is measuring.

### Fixed — a failing health verdict can no longer satisfy the gate
A recorded `verdict: "fail"` was accepted as "the health gate ran", so a run could settle
with measured violations on the record. A failing verdict now raises a gap at every scale
and the latest verdict rules, so a later passing run closes it; `warn` is not treated as a
violation. Direct `node test/x.mjs` invocations are recognised as a subset test run —
previously only scripted runners counted, so running one test file by hand looked like
running nothing.

### Added — `asf_decisions action=bootstrap`, and the register follows the code
A project with code and no decision register now gets one bounded, once-per-run offer with
concrete candidates (foreign ADRs, configs, CI, dependencies, plan docs, commits that
changed course) and a ceiling on how many records to write. **ASF never writes a recovered
record itself** — it can prove what exists, not why, and a register of invented rationale
is worse than none. A record with `origin: backfill` is accepted only when it cites
`source: [path or commit]`; without provenance it is demoted to `proposed` and flagged.
The offer is guidance, not a gap: it never says stop and cannot make a run never close.
Register lookup now resolves to the project the changed code lives in (card, settle and
observer), so a session opened in a parent repo is judged against the decisions that
govern the files it touched.

### Fixed — lifecycle events are no longer written as rejected evidence
`run-open`, `steer`, `park` and the rest are transcript history, but they were passed
through the ledger validator, came back `unknown entry kind`, and were written up as
`asf-evidence-rejected` — a fake failure on every event, which is exactly the noise that
makes a real rejection impossible to see. The kind now decides the channel, and a kind in
neither vocabulary is still reported as the bug it is. `test-evidence-kinds.mjs` asserts
both directions: every kind the gates demand has a producer, and every kind ASF emits is
accepted somewhere.

### Fixed — review that cannot say what it checked is not a review
A reviewer returning zero findings with no stated scope was counted as a clean review. It
is now recorded `not run`: agreement you cannot audit is not evidence of anything, and
"the reviewers found nothing" was reachable by a reviewer that read nothing.

### Fixed — a release is not done until the copy pi loads matches the registry
`~/.pi/agent/settings.json` pins the extension package by exact version, and a stale pin
reinstalls an old version over the one you just verified. The local install gate now checks
that pin and repairs it structurally, preserving every other setting and never inventing a
package entry pi was never asked to load.

### Documentation
The M2 planted-assumption experiment is recorded in `docs/experiments/` (2 of 3 reviewers
found the unstated single-writer assumption; the run that failed to engage the artifact is
recorded too). The adoption plan's test attributions are corrected — several named
assertions in files that do not contain them, which is the stale-claim failure the plan
itself warns about.

## [0.19.0] - 2026-11-10

### Added — a required question goes where the answer actually lives

PO policy: a long loop runs until the work is resolved, and stops for input only when
ASF genuinely cannot supply it — and when it does stop, the ask is *context aware*. The
completion gate's park and the third identical refusal previously said "ask the PO
through the project's input channel", which in a live ticket session means a question
in a CLI nobody is watching while the ticket system has a needs-input queue built for
exactly that. `lib/gates/input-channel.mjs` now resolves the channel from what the
session actually has — the items it claimed (the tracked-work observer already knows)
and the tools the host reports via `getAllTools()` — and both park sites render their
message through it. With a work item claimed and a park tool registered the agent is
told to call *that* tool on *that* item and keep going; with no such channel it is
told to ask once in the conversation and keep going. Nothing is hard-coded to
Betamaxx: the park tool is found with the same `gate.tracked.park` matcher tracked work
already uses, so a Linear or Jira project is routed by its own tool, and a missing
integration says so instead of naming a tool that is not installed. A tool that parks
for a verdict (`flag_review`) is never the ask channel — routing a question through it
would tell the PO the work is invalid when it is merely blocked.

Also fixed, found while wiring it: `bmx_flag_review` was missing from the park matcher,
so an agent that legitimately flagged a ticket ("this claim is not a real issue") was
still counted as having work dangling and was steered to park it — a false alarm on a
disposition it had already taken, and the one steer no amount of extra work satisfies.

### Added — a release must prove the copy that runs is the copy it published

`scripts/install-local.mjs`, run as the last gate of `npm run release`: it installs the
just-published version into the live pi extension directory and compares the installed
tree against the registry tarball file-by-file by sha256.

This gap cost a whole release cycle. 0.18.0 passed typecheck, the full suite, the
"publish landed" poll and the published-artifact load test — and this machine kept
running 0.17.13, because `~/.pi/agent/npm/package.json` pinned `^0.17.13` and a caret
on a `0.x` version does not span a minor. Every gate read the registry or the working
tree; none read the copy pi actually loads, so fixes believed installed and verified
were not installed at all. Version equality alone would still pass a hand-patched or
partially installed copy, which is why the check compares bytes.

## [0.18.0] - 2026-11-10

### Added — a run can be completed, and tests can no longer corrupt one

Two defects from the same blind spot: ASF tracked runs but had no honest way to finish
one, and nothing stopped a test suite from writing into the run state a real user reads.

**`asf_run_complete`.** A run used to close only when the completion gate happened to
pass at settle time, or when a human typed `/asf abort`. There was no action the agent
could take, so a run that had genuinely finished stayed open and swallowed the next
piece of work. This project carried a single run opened three days earlier that had
absorbed several releases and unrelated changes in its `filesTouched` — the gate was
judging today's work against Monday's evidence, so nothing settled and the loop never
closed. The user's instruction was to "properly complete and mark as completed every
run, so you judge the next run separately again".

- The tool asks the **same question the gate asks**: `evaluateRun()` is now the single
  judge, called by both `judgeAndSettle` and the tool. Two implementations of "is this
  run done" drift, and the drift reads as a run the agent closed that the gate still
  counts as open.
- It **fails closed**. An unfinished run is refused with its gaps listed and stays open
  with its evidence. A tool that closed runs on request would be an exit around the
  completion gate, which is worse than the bug it fixes.
- `closeRun()` is the only writer of run history and the only clearer of `current`, and
  it records `by` — `auto` from the gate, `asf_run_complete` from the agent — so a close
  that happened outside the gate is visible in evidence.

### Changed — the decision register follows the code

`asf_decisions` wrote to the session workspace. Working in a repository opened from a
parent directory (a home directory that is itself a checkout is the common case) put the
ADRs one level above the project they describe, where nothing would ever read them. The
register is durable knowledge *about* the code, so it now resolves through `measureRoot()`
like every other measurement. `currentRegister()` was deleted: it announced itself as
"exposed for the handlers" and had no callers.

### Fixed — a test suite could not touch a live run

A suite that forgot to `chdir` wrote into the developer's real run state: `/etc/passwd`
and a scratch path appeared in the live run's `filesTouched`, and the reviewer saw them
in the run card. State is the one thing a test corrupts with no visible failure — the
assertion passes and the user's screen is wrong.

- `homeStateDir()` honours `ASF_STATE_ROOT`, and `test/harness.mjs` sets it to a fresh
  temp directory before the extension loads. The whole suite is now provably inert
  against real state, verified by diffing the state tree across a full `run-all`.
- `test-decisions.mjs` pins **both** behaviours — the real `.asf/state/<key>` layout when
  unset, the override when set — so the isolation cannot silently defeat the assertion
  it was added next to.

`scripts/verify-published.mjs` also states its own boundary: `scripts/` does not ship, so
a user cannot run it, which is fine only because every assertion it makes delegates to
shipped code (`lib/gates/evidence-coverage.mjs`, the same module behind
`asf_evidence_check`). A check added there that cannot be reproduced from an install is a
check only a releaser can run.

## [0.17.13] - 2026-11-10

### Changed — no scale means small (PO policy)

PO decision: "If no scale, assume small." `currentScale()` used to judge a run with no
recorded scale as **standard**, unless an earlier `/asf small` in the same process had
flipped a global flag. That meant two things. First, the same unscaled run got a
different verdict depending on what a previous run had done. Second, the run card
printed the raw field ("unset", or "small") while the gate demanded standard's
`/asf health` and full suite. The stuck sessions this produced were unreadable from the
card alone.

- `currentScale()` now returns small for an unset scale. Explicit `large` still judges
  major and explicit `standard` still judges standard, so the change cannot widen.
- The default in `sessionFacts` and `decideSettlement` is small too, so a caller that
  forgets to pass a scale gets the floor rather than a ceremony nobody chose. The floor
  itself still holds: changed source still needs a real test run.
- The run card, `/asf status` and `/asf verify` print the scale the gate judges by.
  Unset reads "unset — judged small", never a bare "unset".
- `runtime.lastScaleWasSmall` and `markSmallScale()` are deleted. With small as the
  default the flag decided nothing, and a field that reads as load-bearing but is not
  is how this disagreement started.

The reachable scale-less path is pinned: `/asf verify` calls `setPhase` without a scale,
and `setPhase` only writes the field when given one. `test/test-scale-policy.mjs` covers
that path end to end, along with the judged scale, settlement and the card. Five
mutations, one per layer, are each caught.

## [0.17.12] - 2026-11-10

### Fixed — the health gate measured the wrong project

`runRoot()` is the git root of the session's cwd. That is right for where run state
lives and wrong for what to measure, and a session launched from a home directory that
is itself a git repo (common: `pi` started from $HOME with checkouts underneath) had
every health gate measure the parent. It did: `asf_health` reported 363 model findings
from an unrelated project checked out under $HOME, and eslint, madge and jscpd each
timed out at 120s scanning a tree nobody was working in — a verdict of NOT healthy on a
repository that is, in fact, healthy.

A gate that measures the wrong directory is worse than no gate: it blocks on findings
in code the run must not touch, or waves through the one project that is actually
unhealthy. `measureRoot()` now answers from what the run already knows — the git root of
the most recently touched file — and is used by the three paths that measure
(`asf_health`, `/asf health`, `/asf verify` Gate 8). It is deliberately **not** used to
locate state: a root that moved mid-run would orphan the run's own ledger, so
`runRoot()` still follows the session for that.

A touched path outside the workspace (a scratch dir, an unrelated checkout) does not
move the measurement either — the session root is the safe answer. `test/test-measure-root.mjs`
pins both on throwaway repos rather than this machine's $HOME, and both guards are
mutation-caught.

## [0.17.11] - 2026-11-10

### Added — the coverage invariant is checkable from the installed package

`test/test-evidence-kinds.mjs` asserts that every evidence kind a gate requires has a
producer, but `test/` is not shipped — so a broken or half-updated install reproduced
the one bug that suite exists to catch, and the session inside it had no way to find
out. It could only be steered, at the same gap, forever. Three callers now ask the one
question, against one definition in `lib/gates/evidence-coverage.mjs`:

- `asf_evidence_check {kind?}` — the agent's own check. An unproducible **required**
  kind is reported as a broken install rather than as work to keep attempting, because
  no amount of effort records evidence nothing in the package can write.
- `scripts/verify-published.mjs` — a fourth release section, run against the copy npm
  just installed. A tarball missing a producer now fails the release instead of
  shipping the loop to every user while the repository stayed green. A section that
  throws is a failed check, never a silent pass.
- `test/test-evidence-kinds.mjs` — asserts the answer, and asserts the shipped module
  derives the same required set an independent derivation in the test does.

### Fixed — the coverage checker could not say "no"

Writing the negatives proved it: five mutations of the new module initially passed the
suite, because every assertion asked only whether a producer was **found**. A checker
whose regex matches everything, or that counts the ledger as the producer of its own
kinds, is invisible to a suite like that — and `requiredOrphans: []` was invisible too.
It is now tested against fixture packages where the answer is known to be no: a kind
nothing writes, and a kind only the ledger mentions. The tool's two paths no session
hits on a good day (broken install, check that threw) were untested for the same reason
and are now driven directly.

### Changed — the code-health config lives under `.asf/`

`.asf/code-health.json` is now read first, with the root `.asf-code-health.json` still
honoured so a project configured before the move does not wake up on defaults after an
upgrade. This was the last ASF-authored file still written to the project root, and
"everything ASF authors lives under `.asf/`" is one decision with one table of names:
the path now comes from `FILES` in `lib/home.mjs` rather than a literal in the reader,
which is how a file ends up written in one place and read from another.

### Fixed — the shipped CHANGELOG said nothing

The 0.17.10 section was the release script's untouched placeholder, twice over: an
empty `### Added` above a second heading for the same version. `CHANGELOG.md` ships in
the tarball, so that is what a user reads. `test/test-antipatterns.mjs` now asserts the
shipped version has a section, that it is not the placeholder, that it has real prose,
and that no version is listed twice — making "Vague commits or CHANGELOG placeholders"
an enforced anti-pattern rather than a listed one.

Also: `test/harness.mjs` passes a jiti loader to suites, since a `.mjs` suite cannot
import ASF's own `.ts` modules and each suite spinning up its own loader is how two
different loaders start disagreeing; `lib/code-health/tools.d.mts` added (an imported
`.mjs` used from `.ts` needs a declaration or typecheck fails).

## [0.17.10] - 2026-11-10

### Added — the browser evidence the completion gate demands can now be produced

The gate required browser verification whenever a UI file was touched (in two places:
the ledger's completeness check and the delivery report), but the only registered
tools were `asf_health` and `asf_decisions`. Any session that edited a tsx/html/css
file was steered at a gap it had no hands for — the health loop, one layer down. Two
agent-callable tools now close it, mirroring `asf_health`:

- `asf_browser_verify {page, action, assertion}` — **refused** unless the session
  actually drove a browser. ASF sees every `tool_call`, so a verification with no
  browser in the room is a claim about a page nobody opened; `browser_diagnose` and
  `browser_logs` do not count, because they report on the daemon, not on the surface.
  All three fields are required: that is the minimum a reviewer can go and repeat.
- `asf_browser_na {reason}` — **refused** when the reason is a placeholder (`n/a`,
  `none`, `not applicable`, one word). A structured n/a is worth having only if a
  reviewer can disagree with it; otherwise it is an exit around the gate.

Recorded, not claimed: a tool that accepted "I verified it" would make the gate
decorative, and a decorative gate reads as coverage. The rules live in
`lib/gates/browser-evidence.mjs` (pure, testable without a browser) with a thin
extension adapter, mirroring the `measure.mjs` / `health-record.ts` split.

### Fixed — five ledger kinds had no producer

`browser-verify`, `browser-na`, `clean-room`, `spec-transition` and `perf-scenario`
could be validated and rendered but never written. Now:

- `spec-transition` is **observed** from the real `update_spec_status` call, so the
  delivery report renders actual transitions without anyone remembering to record them;
- `clean-room` is observed from the release pipeline (`npm run release` /
  `verify-published.mjs`) together with its exit code — the definition-of-done line
  about the shipped artifact no longer promises evidence nothing can produce;
- `perf-scenario` is **dropped**: no producer, no requirement, five mandatory fields
  nobody could fill.

### Added — the runner can no longer read a suite as green when it never checked

`test/harness.mjs` honoured only `export async function run()` returning false. A
suite exposing its verdict as the module's default export was ignored entirely —
which is how a new suite reported "51 passed" while enforcing nothing. The runner now
reads `run()`, a boolean default, or a boolean `ok`; a suite claiming `selfReports`
must contain a real failure channel (`process.exit(1)` or `node:test`) or it warns;
and a suite with none of these is named instead of passing silently. The whole class
is pinned by `test/test-evidence-kinds.mjs`: every ledger kind must have a producer in
the shipped package, and no kind a gate *requires* may be an orphan.

Also: `tool-call.ts` passed the 300-line ceiling and its work observers moved to
`lib/extension/handlers/observe-work.ts`, with pi's `Input`/`Event` shapes declared
once in `observed.ts` instead of three times; `String(undefined)` → `?? ""` in the
evidence builders (the literal "undefined" reads like a value and passes a presence
check); and a redundant guard in `placeholderReason` was deleted because no mutation
of it failed a test — a guard that cannot be failed is decoration, not protection.

## [0.17.9] - 2026-11-10

### Fixed — the health verdict now reports what it measured, not what it planned

`asf_health` recorded `scanned` as `rows.length`, and `rows` contains every metric
the gate **planned** — including the ones that never ran because a tool was not
installed. Three things followed, all reported from the same observation:

- the ledger's `scanned 0` rule ("examined nothing, not a pass") was unreachable
  while any metric was planned, so a run that measured nothing was recorded as a
  nine-metric audit;
- the tool's own summary line contradicted the report printed directly above it —
  `NOT healthy — 8 metric(s) skipped` followed by `9 metric(s) measured`;
- the honest number already existed and was thrown away: `verdict()` returns
  `ran`, counting pass+fail and deliberately **excluding** error rows, but
  `runHealth()` never returned it.

`runHealth()` now returns `ran` and `skipped`, and the recorded count comes from
them. Counting `status !== "skipped"` was the tempting fix and is wrong: it counts
errored rows as measured, which puts the report, the ledger and the summary line
back out of agreement — the exact disagreement being fixed. A metric that does not
apply (no persistence in the project) is neither ran nor skipped.

### Changed — every path that produces a health verdict records it

`/asf health` and the Gate 8 section of `/asf verify` printed the measurement and
left no trace, so a human could run the command, read HEALTHY, and leave the
completion gate's health gap open. Recording was the `asf_health` tool's private
privilege; it is now shared, with `by:` saying who produced the verdict
(`tool` / `command` / `verify`). The counting rules and the summary wording moved
to `lib/code-health/measure.mjs` — pure, dependency-free, and tested without a
harness — so the three callers cannot drift into contradicting each other again.

A verdict over zero metrics says so out loud: *"Nothing was actually measured — a
verdict over zero metrics is not a pass, and installing the missing tools is what
closes this properly."*


## [0.17.8] - 2026-11-09

### Added — the tracked-work vocabulary is the project's to widen

`gate.tracked.claim` / `.resolve` / `.park` / `.wait` in `.asf/config.json` add patterns
(string or array) to the built-in matcher set. They **widen, never replace**: a project
that typos `claim` would otherwise stop tracking its own claims, and a floor that a
stray character in a config file can switch off is not a floor. Non-string entries are
ignored rather than compiled in.

`lib/gates/tracked.mjs` now says plainly what it is — **a tripwire over tool names and
the agent's own prose, not an authority over ticket state**. It cannot see a tracker's
database and its vocabulary is a small illustrative set; a tool named `grabWorkItem` is
invisible until the project says so. The asymmetry is deliberate: a miss costs a run
that settles early, a false alarm costs one steer.

### Fixed — a matcher that matched everything

`matchersFromConfig()` built the wait matcher as `alternatives + "|" + extras` even when
`extras` was empty, leaving a trailing `|`. An empty alternative matches the empty
string, so **every progress message in every project would have been read as a
confessed wait** — a gate that trips on everything is as useless as one that trips on
nothing, and much harder to notice. Every union is now guarded by a length check, and a
test pins that no matcher matches `""`, `"anything at all"`, or a config entry of
`""`/`undefined`/`null`. Caught by the suite that checks a benign progress message is
not a wait, and mutation-verified: restoring the dangling `|` fails two checks.

## [0.17.7] - 2026-11-09

### Fixed — "waiting" is now illegal as a disposition

An agent working a claimed ticket ended its run with this on the reviewer's card:
*"Left In Progress at testing — deliberately not resolved, because the user-visible
outcome still isn't confirmed, and not parked, since you told me not to ask you for
anything."* Nothing blocked it. The run closed and the work stopped. That is a stop
wearing a status report, and it is the failure this whole process exists to prevent.

The agent did not misbehave; it had no legal state to stand in. The stop policy names
four things that may halt a run — plan approval, an irreversible action with no safe
path, an unobtainable credential, a deliverable-shaping either/or — and **"the other
side has not replied" is none of them**. A contract that forbids stopping without
naming what to do instead does not produce persistence, it produces paragraphs. And
the agent had correctly noticed that parking *looks like* asking, so a "don't ask me"
directive seemed to forbid the one move that was legal.

- **New gate: `lib/gates/tracked.mjs`.** Claims, resolutions and parks are read off the
  tool *name* (`bmx_claim`, `submit_resolution`, `request_input`, …) — never off shell
  text, because `git commit` is not a claim. A claimed item with no disposition is a
  gap that keeps the run open, and the agent's own progress text is read for the
  confession ("deliberately not resolved", "not parked", "waiting for their reply",
  "it flips the moment…"), which raises it on the spot.
- **The three dispositions, all of which the agent can take itself**: park it where a
  reviewer sees it (parking is a *marker*, not a question, so "don't ask me" never
  forbids it); take the next action you control; or, if you are genuinely waiting on
  someone else, make the wait end in one step — the test that will confirm the outcome,
  the draft mail — and keep going. The gap text offers all three.
- The gap is worded so it cannot route itself into the report: it names no slash
  command and never says "ask the PO", because a gap that reads like a human action is
  filtered out by `splitGaps` and would make the guard decorative. Pinned by a test.
- **Contract text now lives where the agent reads it.** The no-stops policy existed only
  in a source docstring; `06i-completeness.md` (always-read) now carries the rule and the
  DoD has a line for it, with the case study in `06f-stability.md` §6. The always-read
  load stayed inside its budget (75,953 / 76,000) — detail moved out to 06f rather than
  the budget being raised.

### Fixed — declaration files that lie about their module

`lib/gates/blockers.d.mts` still declared `RULE_TEXT` / `refusalStage` /
`__resetRefusals` after they moved to `refusals.mjs` (0.17.5); `lib/gates/settle.d.mts`
still declared `MAX_IDENTICAL_STEERS` after the cap was deleted; `lib/activation.d.mts`
still declared `SOURCE_EXTENSIONS` after it was removed. A `.ts` importing any of those
typechecks clean and gets `undefined` at runtime — the compiler is the tool that is
wrong. New suite **`test/test-declarations.mjs`** loads every `.mjs` beside each `.d.mts`
and fails on any declared export the module does not have. It found the third case on
its first run.

### Fixed — the test runner could not fail

`test/harness.mjs` carried `const ok = globalThis.__tools = tools` where it meant to
capture the suite's verdict: `ok` was the tools array — always truthy — and the return
value of `run()` was discarded. A suite reporting failure by returning false printed its
FAILs and was still counted green, which is precisely the bug the comment above it
claimed was fixed. The verdict is now captured, and the new declarations suite depends
on it.

### Also

- `06i-completeness.md` still described the old steering rule ("a gap is steered once,
  never twice") after 0.16.0 removed the cap.

## [0.17.6] - 2026-11-09

### Fixed — a gate that crashes is not a gate that passed

Both tool handlers were fenced with `catch { return undefined }`, which had two consequences of
exactly the kind ASF bans:

- A failure inside the **suite recorder** lost the run silently, after which the publish and
  completion floors demanded a run that could never register — an unclosable gap with no visible
  cause.
- Because pi's own semantics for a crashing `tool_call` handler are *block*, returning "allow"
  overrode them: anything that made the earlier code throw — a config read, the activation
  observer — disabled the **irreversibility floor** for that call.

Now:

- `reportGateError(gate, e)` records a `gate-error` ledger entry and tells the agent once, as a
  steer: *"That is not a pass — the check failed to execute."* Guidance, never a stop.
- When the gate dies **before** the blockers ran, `failSafeBlock(input)` re-runs just the rules
  whose `failSafe` is `block`, from the raw tool input alone, with no context to fail on. A secret
  in a tracked file is still refused when the gate itself is broken; ordinary work is never
  stopped by an internal error.
- A fail-safe rule that *itself* throws refuses ("could not run") rather than reporting nothing
  found.

Three assertions pin this, each mutation-tested: removing the report, the fail-closed call, or the
crash-refusal each fails a test.

- `lib/gates/blockers.d.mts` still declared `RULE_TEXT`, `refusalStage` and `__resetRefusals` after
  they moved to `lib/gates/refusals.mjs` in 0.17.5 — a `.ts` could import them, typecheck, and get
  `undefined` at runtime. The declarations now match the modules.

### Removed

- Dead export `SOURCE_EXTENSIONS` from `lib/activation.mjs` (no importer).

### Documented

- **The loaded copy is not the shipped copy** (07-release.md step 7 + hygiene rule): `pi` loads
  extensions once at process start, so a session open across a release keeps enforcing the previous
  version and quoting text that no longer exists in the source. Confirm which build is loaded — a
  string unique to the new release, and the process start time — before concluding a released gate
  is broken. That is the difference between "the fix did not work" and "the fix is not running", and
  it cost hours chasing a phantom bug in 0.17.5.
- `06i-completeness.md` pointed its drift example at `lib/gates/ledger.mjs` / `deriveSuiteRun()`;
  those moved to `lib/gates/suite-runs.mjs` in 0.17.5, and the doc — which is about not trusting
  stale references — was itself stale.

## [0.17.5] - 2026-11-08

### Fixed — the publish floor could be satisfied by a green that proved nothing

`push-without-recorded-green` asked for "a recorded full-suite green run in this
session" and accepted **any** green: a run from before the last edit, or a green
from earlier in the session that a red had since overtaken. It also disagreed
with the completion floor, which already required a passing run *started after
the last source change* — so the two ASF floors had different ideas about what
"green" means, and the weaker one guarded the only irreversible action.

The floor is now the same rule in both places: the **last** full run, green, and
started after the last source mutation. Passive mode keeps it — "no steering"
suspends guidance, not the irreversibility floor. The refusal now names which
condition failed (`no full-suite run recorded` / `the last full suite run exited
1` / `the last full-suite green predates the most recent source change`), so the
autonomous exit is the specific action that closes it.

New assertions drive the real `tool_call`/`tool_result` pair rather than a
hand-built context, which is what pins the *recorder*: a floor whose demanded
action does not actually close it is the 2026-11-07 bug class, and the previous
tests could not see that path at all. Six mutations of the new guards (accept a
red, accept a stale green, take the oldest full run, drop the mutation clock,
never record the run, never set the clock) each fail at least one test.

### Changed — two modules split to stay inside the limit ASF enforces

`lib/gates/ledger.mjs` (327 lines) and `lib/gates/blockers.mjs` (301) crossed the
300-line file limit, so the two halves that changed for different reasons were
separated: `lib/gates/suite-runs.mjs` (suite-run recognition and the green-run
rule) and `lib/gates/refusals.mjs` (the refusal ladder and its per-rule text).
Both are imported explicitly — no re-export shim with two ways to reach one
function.

### Removed — `deriveHealthRun`

It recognised a health run by matching `/asf health` in a command's text, which
is the class of bug that produced the endless loop of 2026-11-07: a human slash
command the agent has no tool for. The `asf_health` tool appends the `health-run`
ledger entry directly, so nothing called it and nothing tested it.

## [0.17.4] - 2026-11-07

### Changed — ASF now passes its own health gate, and the gate measures the right tree

ASF demanded code health of every project and did not meet it itself: 74
violations across `index.ts` and `lib/`. This release is that debt paid, and two
real defects the work exposed.

- `index.ts` is 37 lines of wiring. The extension lives in `lib/extension/`
  (`commands/`, `tools/`, `handlers/`, plus state, specs, evidence, steer, run),
  so no single file is the whole extension any more.
- `lib/gates/`: the parameter table moved to `params.mjs` (the table reads as a
  table), `clamp`/`round2` to `math.mjs` (the table's calibrations use them, and
  importing them from the resolver was a cycle), `ledger.mjs`/`drift.mjs`/
  `settle.mjs`/`blockers.mjs`/`config.mjs`/`declarations.mjs`/`authority.mjs`
  decomposed into named pieces instead of one long function each.
- `lib/code-health/`: `model-scan.mjs` (file selection and I/O), `model-patterns.mjs`
  (the pattern tables), `eslint-run.mjs` (config generation and counting),
  `exec.mjs` (the bounded spawn).
- Functions that took four or more positional arguments now take one options
  object (`deriveSuiteRun`, `deriveHealthRun`, `transition`, `evaluateBlockers`,
  `planMetrics`, `walk`, `resolveParam`'s resolvers, `runBlocker`).

### Fixed — the gate wrote into the repo it was measuring

`jscpd`'s report went to `<project>/.asf-code-health-tmp`, which dirtied the
measured tree (it tripped ASF's own dirty-tree guards) and shipped a committed
temp file. It now writes to a `mktemp`-style directory under the OS temp root and
removes it. A test asserts the `--output` path is outside the project and that
the directory is gone afterwards.

### Fixed — `asf_health` measured the session's cwd, not the run's project

A session started in `$HOME` measured the whole home directory: 960 "handwritten
cascade" findings from `site-packages`, and a fail verdict on a healthy repo.
Every consumer of "the project" (the health tool, `/asf health`, `/asf verify`,
`/asf config`, the project config read, the git-dirty probe, `PLAN.md`, spec
traces) now goes through `runRoot()` — the git work-tree root — instead of
`process.cwd()`. A test fails if any extension module measures
`process.cwd()` as the project.

### Fixed — the tests stopped covering half the extension

The text-assertion helpers in `test-operator-surface.mjs`, `test-browser-touched.mjs`
and `test-model-gate.mjs` read `lib/extension/*.ts` non-recursively, so after the
split the `commands/`, `tools/` and `handlers/` modules were outside every text
assertion. They read recursively now.


## [0.17.3] - 2026-11-07

### Fixed — the tools were installed but never found

0.17.1 declared the toolchain as dependencies and then looked for it in
`<pi-aia-asf>/node_modules`, which does not exist in the layout pi installs: npm
**hoists** a package's dependencies into the shared `node_modules` one level up.
Every tool therefore read as missing, the gate fell back to a system `eslint` (v8,
which rejects `--no-config-lookup`), and eight metrics came back as errors — the
same blindness as before, with a different message.

- Resolution now walks the real `node_modules` chain from ASF's package directory
  upward, exactly as node resolves, so a hoisted install is found. `require.resolve`
  is only a last resort: on Debian it escaped the install entirely and returned
  `/usr/share/nodejs/eslint`.
- Packages with no `main` field (jscpd) can never be found by `require.resolve`;
  the directory walk finds them.
- A regression test builds the hoisted layout on disk and asserts all five tools
  resolve inside the install prefix, with ASF's copy winning over PATH.

## [0.17.1] - 2026-11-07

### Added — the health gate ships its own toolchain

The gate could not measure. `detectTools()` looked only in the **project's**
`node_modules`, so on any repo without eslint/jscpd/madge it produced eight
"tool not installed" skips and reported a level it never examined — and a run
that changed source could be judged by metrics that were all missing.

- **The tools are now ASF's own dependencies**: `eslint`, `eslint-plugin-sonarjs`,
  `@typescript-eslint/parser`, `jscpd`, `madge`. Installing pi-aia-asf installs
  them, so a fresh clone with an empty `node_modules` is still measured.
- **ASF-first resolution** (`lib/code-health/tools.mjs`): ASF's copy, then the
  project's, then PATH. The same metric means the same thing in every repo, and a
  project pinning its own eslint still wins.
- **No more `npx --no-install`.** Every metric tool is spawned by absolute path
  from the resolved install; `npx` could silently run a different version or none.
- **Deterministic eslint.** The gate writes its own flat config to a temp file and
  runs with `--no-config-lookup`, so a project's eslint config cannot change what
  is measured. The sonarjs plugin and TS parser are imported by absolute file URL
  from ASF's install.
- **Missing tools name the package** ("jscpd is not available — it ships with
  pi-aia-asf; reinstall the package") instead of a bare "tool not installed".

### Fixed

- **A sonarjs metric counted the wrong rule id** (`cognitive-complexity` instead
  of `sonarjs/cognitive-complexity`), so it always reported zero violations.
- **Vendored trees were scanned as project source.** The model level walked
  `.local`, `site-packages`, `.venv`, `vendor`, `third_party`, `target`, `Pods`
  and every other dot-directory: from a home directory it produced 958 findings
  from Python packages belonging to nobody's project. Those trees are skipped now.
- **A circular dependency in ASF itself** (`code-health/model.mjs` ↔ `run.mjs`,
  via a dynamic import) — found by madge the moment it could actually run. The
  file walker moved to `lib/code-health/files.mjs`, imported by both.

## [0.16.1] - 2026-11-07

### Fixed — the 2026-11-07 endless completion-gate loop

The gate could steer forever for **no reason the agent could act on**. Two defects
compounded: it demanded `/asf health`, a pi slash command an extension has no tool
to invoke (so the gap could never close), and the "never the same steer twice" rule
keyed on a fingerprint that concurrent saves kept wiping, so the identical ask was
re-issued indefinitely. Both are now structurally impossible.

- **A gate can only block on something the agent can close.** `splitGaps()` sorts
  every gap into blocking (agent-closable) and reported (needs a human), and it
  runs **once, after all gaps are assembled** — splitting earlier let gaps pushed
  afterwards skip the filter. A human-only gap is rendered as "not verified / not
  blocking" and never resumes the run.
- **`asf_health` tool.** The health gate's only demand is now an agent-side tool
  call, which records a verdict in the ledger. No gate asks for a slash command.
- **A warranted loop has no ceiling.** A real, agent-closable gap is steered for as
  long as it stays open; the old `exhausted` paths that settled the run with gaps
  still open are gone. Repeats are worded as repeats ("still open after N previous
  steers") instead of pretending to be new information, and no message claims a
  bound ("of at most 12") the gate does not have.
- **The steer count is in-process memory, and state.json is only its record.** A
  corrupt, raced or hand-edited file can no longer reset the count or erase a
  fingerprint; `Math.max` on read plus a monotonic merge on write make it
  non-decreasing.
- **Saves are serialized per project and merged under the lock.** Eleven paths
  read-modify-write the state file; the write is now queued per project and
  re-reads the file inside the lock, merging the monotonic fields (steer count,
  fingerprints, touched files, last mutation). Write-to-temp-then-`rename` means a
  reader never sees a half-written file.
- **A corrupt state file can no longer disable the gate.** Wrong-typed fields used
  to throw inside the settle handler, and the catch-all returned `undefined` —
  which settled the run as if the gate had passed. Fields are coerced on load and
  on both sides of a merge, and an internal error is now surfaced once as
  `asf-gate-error` (deduped by error class, since an I/O message carries a random
  tmp filename) instead of being swallowed.
- **A park is asked once per (run, reason)**, and guidance-only continuations are
  bounded per run, so no secondary nudge can hold a session open on its own.

Every guard above is pinned by a test that fails when the guard is removed.

## [0.15.1] - 2026-11-06

### Fixed

- **Specs are read under the same project key as run state.** 0.15.0 moved run state
  to the git-root key but still read pi-vigilant's spec store by cwd basename. From
  a subdirectory, the trace floor for specs met in the run therefore looked in the
  wrong store and found nothing. pi-vigilant 0.9.0 writes the store under the root
  key, and ASF reads it there. It falls back to the basename location, so an older
  pi-vigilant does not leave ASF unable to see specs. Both suites pin the same
  key vectors.

## [0.15.0] - 2026-11-06

### Added

- **ASF is an active process for all software work.** The first source change in a
  session opens a `small` run automatically. That includes edit/write tools, bash
  writes into source (heredoc, `sed -i`, scripted `open(...,'w')`, redirect, `tee`,
  `patch`, `cp`/`mv` into source) and hand-spawned `pi -p --no-extensions` workers
  that have write tools. It works in non-git directories too. Docs, assets,
  `node_modules` and loose `/tmp` scratch never activate it.
- **Completion floor.** A run that changed source cannot settle until a passing test
  run that started *after the last change* is recorded. A lint or typecheck does not
  count, and neither does a green run from before the edit. A spec marked met during
  the run must carry `trace.outcome` + `trace.codePath`; specs met in earlier runs
  are not re-checked.
- **Run card.** A compact card (≤1.5 KB, registered in the load budget) with the
  phase, the open items and the single next step. It is shown when a run opens and
  again at `session_start` and `session_compact`.
- **`/asf passive`** silences steering for the current run only. The next source
  change re-arms ASF.
- **Refusal ladder.** The same refusal escalates: refuse → refuse with the rule
  text → park for the PO. `/asf status` shows when a run is parked or passive.
- A long stretch of untested source edits gets exactly one steer naming the files.
  It never blocks.
- `test/test-active-process.mjs`: an end-to-end suite that drives the real handlers,
  with a negative control per guard.

### Changed

- **No stops.** The fixed continuation cap (`MAX_CONTINUATIONS = 2`) is replaced by
  de-duplication: each gap is raised once, an identical steer is never issued again,
  and after `MAX_STEERS` (12) gaps are reported as unverified instead of re-asked.
  The run only parks for plan approval, an irreversible step with no safe path, an
  unobtainable credential, or a deliverable-shaping choice.
- `test-subset-without-impact` now steers instead of refusing (71 blocks in 18 days,
  64 of them repeats). The secret, push-without-green and destructive-git refusals
  are unchanged and still apply in passive mode.
- **Run state is keyed by the git work-tree root** (absolute path outside git), not
  the cwd basename. `cd` into a subdirectory no longer loses the run, and two
  checkouts with the same folder name no longer share one. A run saved under the old
  key is migrated once. Spec memory is still read under pi-vigilant's key.

### Fixed

- Deleting or corrupting `state.json` no longer silently ends enforcement. The
  active run is restored from memory and the deletion is recorded. `/asf abort`
  remains the way to end a run.
- A malformed `agent_before_settle` event no longer requests a continuation.
- The test harness ignored a suite's `false` return value, so a suite that reported
  failures still counted as passed.

## [0.14.1] - 2026-11-04

### Fixed

- **The completion gate's continuation now actually runs** (pi 1.5.0). pi 1.5.0 validates
  boundary continuations instead of trusting them: it commits the entries an extension
  returns, rebuilds the context, and only continues when `canContinue` holds. The gate
  returned a bare `{ continue: true }`, which at settle time means an assistant-terminated
  context with nothing queued, so pi refused it —
  `agent_before_settle requested continuation without runnable model context` — the agent
  was never told what was missing, and the gate's one bounded continuation logged an error
  and did nothing. It now returns a `custom_message` entry (pi projects it to role `user`,
  which is what makes the next provider request runnable) carrying the missing list.
- **The gate only judges ASF runs.** `currentScale()` had no "no ASF run" state, so every
  session that loaded ASF was held to the delivery evidence bar. While the continuation was
  inert this was invisible; once it resumed runs, a plain `pi -p` in a scratch directory
  went from 3s to 181s and was told to close its missing evidence. The gate now reads the
  project's ASF state and stays silent unless a run is active.

### Changed

- The pi peers are pinned to 1.5.0, the version that actually loads the extension.
- `jiti` is declared as a devDependency; the test harness imported it without ever
  declaring it, so a clean `npm install` broke the suite.
- The typecheck gate no longer reuses a cached peer at the wrong version. It printed
  `host types 1.2.0` against a manifest declaring 1.5.0 and still reported OK — the failure
  its own header exists to prevent.
- `scripts/verify-published.mjs` polls for the tarball instead of taking one HEAD request,
  which 404s transiently while a fresh release sits in npm's staging queue.

## [0.14.0] - 2026-11-02

### Added

- **Zero-context reviewer pass** (Phase 4 and Phase 7, large work): six read-only reviewer
  personas in `skills/aia-asf/agents/` (`design-critic`, `codebase-fitness`, `zoom-out`,
  `comprehension`, `implementation-readiness`, `structural`) plus a `synthesizer`, each
  invoked separately, each answering in a fixed finding format (`Blocker | Major | Nit`,
  scope, rationale, recommendation), critique-first and cite-or-abstain. Reviewers are
  given the artifact and the acceptance criteria only — never the conversation that
  produced it.
- `lib/review.mjs` — the review guards as code rather than as prose: citation verification
  (a report whose cited file or line range does not resolve is recorded `not run`, **never**
  "zero findings"), the per-reviewer status table, the finding schema with dedup and
  severity ordering, and A/B duplication permitted only at the blocker boundary. The
  citation guard exists because a read-only subagent was observed reporting a confident
  reading of a file that was never asked for, and exiting 0.
- `lib/parallel-estimate.mjs` — the serial/parallel decision computed from file ownership,
  interface ownership, dependency cycles, serial cost and a bounded schedule. Parallel
  requires no conflicts, at least 3 fully independent packages and a speed-up of 1.3×; a
  job under 30 serial minutes is refused outright, and a refusal is a recorded decision
  rather than an omission. Also carries the P47 switch (`readParallelSwitch`: default OFF,
  `ASF_PARALLEL=1` for one run that does not persist) and the join contract: one lane, the
  full suite after the rebase, no merge conflict resolved by a model, no thread resolving
  its own ticket.
- `lib/gates/drift.mjs` — task-scoped drift: the claims this task's own artifacts make
  about files and symbols are resolved against the code. `no-claims` and `no-artifacts`
  are reported as examined-nothing, never as clean, and only a stale claim blocks.
- `scripts/audit-docs.mjs` — ASF audits its own normative content: relative links, anchors,
  index/directory agreement, routing ("Read when" or an index row) and frontmatter.
- `lib/skill-budget.mjs` + `test/test-skill-budget.mjs` — an always-read byte ceiling with a
  rule-preservation check: procedure, tables and examples may move out of the always-read
  files; a rule statement may not, and a split that drops one fails the suite.
- `references/06m-antipatterns.md` (the complete forbidden list, with a short always-read
  floor left in `SKILL.md`) and `references/06b-appendix.md` (the Rule 9 browser procedure
  and Rule 15 enforcement-class detail).
- `references/05-plan.md` — a work-package table (files owned, interfaces needed and
  provided, depends on, estimated minutes) and a required parallel verdict with its reason,
  recorded even when the answer is serial.
- Rebuilt `test/test-antipatterns.mjs`, `test/test-parallel-switch.mjs` and
  `test/test-operator-surface.mjs` — these had been in the working tree, were never
  committed, and are gone; they are rewritten against the modules they guard. Added
  `test/test-review-guards.mjs`, `test/test-docs-integrity.mjs`,
  `test/test-skill-budget.mjs`, and task-scoped drift coverage in
  `test/test-improvements.mjs`. Every new guard ships with a control that must fail.

### Changed

- The always-read set is 74,777 bytes across `SKILL.md` and the three mandatory references,
  down from 88,515 (−15.5%), with the per-file ceilings and their reasons recorded in
  `lib/skill-budget.mjs` rather than as a bare number. `SKILL.md` itself went 36,931 →
  25,689 bytes; the reduction is duplicated procedure and the forbidden list, not rules.
- Phase 5 presents `PLAN.md` as an artifact (path plus decision summary) instead of pasting
  the plan into chat; Phase 4 now states a reviewer budget (6 reviewers, 2 re-audits, 3
  concurrent, stop and report at 6× the serial token estimate) so thoroughness is bounded
  without being thin.

### Fixed

- `references/06k-performance.md` linked to a research document that is not in the package —
  a dangling link that no test could see until the doc audit was added.

## [0.13.5] - 2026-10-18

### Added

- **`test/test-packaging.mjs` — the artifact gate.** Checks the **packed** file set
  rather than the working tree: `index.ts`, the `skills/aia-asf` skill, its normative
  reference files, the `lib/` modules and the declared `obligations.d/` all really ship;
  every entry `files` allowlists is present in the tarball; `pi.extensions` and
  `pi.skills` point at entries that ship; and no relative import inside the artifact
  points outside it. Nothing could previously see a broken `files` allowlist —
  typecheck reads source files, the suite imports from disk, and `npm publish` only
  reports that the upload was accepted. An ASF release whose skill did not ship is an
  empty deliverable that installs without error.
- **`scripts/verify-published.mjs` — post-publish verification of the published bytes.**
  Installs the just-published version **from the registry** into a throwaway prefix
  (with the peer deps the pi host provides), imports the installed extension, requires
  its default export to be the activate function, and requires the skill and
  `obligations.d/` to be present in the installed copy. Reachable as
  `npm run verify:published`; `BMX_SKIP_PUBLISHED_VERIFY=1` skips it in the release.
- **`scripts/release.sh` now gates instead of announcing.** It used to print
  "✅ Released" on the strength of `npm publish`'s exit code alone. It now polls the
  registry until the version is listed **and its tarball is fetchable**, then runs the
  published-artifact verification, and exits non-zero if either fails.

### Notes

- The import resolver in the artifact gate follows the ESM declaration convention, so a
  `.d.mts` file naming its sibling as `./x.d.mjs` is not reported as a missing module
  (`lib/gates/` ships `.mts` + `.d.mts` pairs).

## [0.13.4] - 2026-10-16

### Fixed

- **The model-blind-spot research (D1/D2) could not be verified against its own
  sources, and now it can.** Both deliverables existed; neither had been audited,
  and the audit found three real problems.
  - **Seven quotations attributed to unnamed sources were removed.** The document
    leaned on block quotes from "a soft-delete design guide", "the tenancy
    source", "a team that learned it the expensive way" and the BEA book — none
    carrying a URL. Checked by exact-phrase search and by fetching the cited
    URLs: `orphans-by-policy`, `humans forget filters … RLS policies forget
    nothing` and `we paid for the trash folder twice` return **zero results**,
    and the InfoQ and O'Reilly fitness-function URLs return **404**. An
    unfindable quotation is worse than no citation — it reads as evidence and
    invites downstream reuse — so each was deleted, and the claims that still
    matter were restated as explicitly-labelled **T3** reasoning. Where a primary
    source says it better, it replaced the paraphrase: the PostgreSQL manual's own
    default-deny text is stronger and checkable than the quote it replaced.
    The deletions are listed in a withdrawn table rather than dropped silently.
  - **What remains is now genuinely T1** — fetched and read this pass, with URLs
    and page numbers: Vernon's four aggregate rules (ArchiLab, ch. 10 pp.
    347–388), the OWASP IDOR methodology, PostgreSQL §5.9, the Thoughtworks
    fitness-function article. `SOURCES.md` gained the Model section with T1/T2/T3
    classes per 02-research.md, including a row that admits the Rails `dependent:`
    option list was **not** read and must be checked before any option name
    reaches normative text.
  - **The inventory had gone stale.** It was measured against `06a–06i` and never
    re-derived after `06j`, `06k`, the gate layer and the model fitness level
    shipped — so it still claimed all 14 classes were invisible. Re-scored against
    the current gate set with the H/M/S enforcement classes: six classes (3
    concurrency, 5 time, 6 numeric/unit, 7 locale, 9 unit-of-work, 12 environment)
    remain **completely uninspected**, and that is now stated as a count instead of
    a mood.
  - **The audit covered only half the mandate.** The spec says "ASF **and
    vigilant**"; vigilant had never been audited. It now has its own table — Loop
    Guardian, spec verification, completeness guard, obligations container,
    tool-timeout abort — and the honest conclusion: vigilant inspects the agent and
    the claim, not the deliverable's design, and by the package boundary it must.
    So for those six classes, **if ASF does not inspect them, nobody does.** There
    is no second line of defence in the other package.
  - **The meta-finding got a status.** "Not one gate asserts the non-effect" was
    written before 06g §3 added non-effect assertions; it now reports itself as
    half-closed — the language exists, the compulsion exists only where a blocker
    was built.
- **`test/test-model-research.mjs` (42 checks) guards all of the above**, because
  the failure mode is recurrence rather than novelty: it asserts the inventory is
  contiguous and fully re-scored, that vigilant is audited, that every sourced
  subsection resolves to a URL or is labelled T3, and that each withdrawn phrase
  survives only as a record and never as a quotation. **Negative control verified**
  — injecting an unattributed quote back into §2.6 makes it fail, and removing it
  passes.

### Notes

- **`npm audit` triage for this line: accepted, not fixed — and the reason is
  recorded rather than skipped.** Three advisories report (1 moderate,
  2 high), all tracing to `undici` and `brace-expansion`. None of them ships
  with ASF: `dependencies` is `{}` and the packed artifact (50 files) contains
  no `node_modules` and neither package. They are reachable only through the
  **peer** host package `@earendil-works/pi-coding-agent`, which is the user's
  own pi installation and is pinned in `devDependencies` solely so typecheck can
  run against a known host version (`host types 1.2.0`).
  - `npm audit fix` would bump that pin. It was deliberately **not** run: it
    would move the host-types version the entire typecheck and suite are
    calibrated against, for no change whatsoever in what an installed ASF
    contains. The fix belongs to the pi release, not to this manifest.
  - Consequence for anyone reading this: an `npm audit` hit on ASF is a signal
    about your **pi install**, and should be resolved by updating pi. Checking
    the shipped surface is the `dependencies: {}` line plus the pack list above,
    not the audit count.

## [0.13.3] - 2026-10-16

### Fixed

- **06j §5 had drifted back into enumerating products.** The section now carries
  the store-agnostic *rule* — the defect is not SQL-shaped, and the distinction
  that makes a hand-written cascade decidable is the **reference** (a row removing
  itself vs one entity deciding the fate of another) — instead of a table of
  store families and a list of ORM dialects. Which dialects the scanner
  recognises is an implementation detail in `lib/code-health/model.mjs`, reported
  per finding so you can see what it looked at; the standard states categories and
  leaves the rest to `model.extraCascadePatterns` /
  `model.extraHandWrittenPatterns`. A recognised dialect is not a coverage
  promise: an unknown persistence shape is a `scanned: 0` you say out loud, not a
  clean bill of health. The fixtures keep naming the concrete dialects — that is
  where a pattern set belongs.

## [0.13.2] - 2026-10-16

### Added

- **The model gate is now store-agnostic in fact, not just in intent.** It knew
  SQL DDL and ORM cascade spellings and nothing else, so in a document, key-value,
  wide-column or search store — where there is no constraint to declare a cascade
  in — the gate was blind to the exact defect it exists to catch. A third rule,
  `handwritten-cascade`, catches the cascade that exists only as code someone
  wrote: a multi-delete driven by a reference to *another* entity.
  - Covered by name: **MongoDB/Mongoose** (`deleteMany({ userId })`, including
    ES6 shorthand), **Firestore** (`collectionGroup` / recursive subcollection
    delete, where the field name is an argument, not a key), **DynamoDB**
    (`DeleteItem` / `BatchWriteItem` keyed by a parent), **CouchDB** (`_deleted:
    true` in a `bulkDocs`), **Redis** (the key-pattern wipe — the prefix *is* the
    parent), **Elasticsearch** (`deleteByQuery` on a parent field). ORM
    declarations were already matched: `CascadeType.REMOVE`/`ALL`,
    `orphanRemoval`, `DeleteBehavior.Cascade`, Prisma/Sequelize `onDelete:
    Cascade`, Django `on_delete=models.CASCADE`, Rails `dependent: :destroy` /
    `:delete_all`, SQLAlchemy `cascade = "…delete…"`.
  - **Near-miss fixtures guard the false-positive side**, because a rule that
    fires on ordinary deletes gets switched off and the coverage goes with it:
    deleting a row by its own identity, `updateMany` by reference, a Redis `del`
    of one known key, a Firestore query on a plain field, a CouchDB bulk insert,
    an ES delete-by-query on a non-reference, a DynamoDB point read — all clean.
  - A fire asks for a **declaration**, it does not claim the ownership is wrong;
    `model.handWrittenCascade: false` switches it off and
    `model.extraHandWrittenPatterns` adds a project's own dialect (an invalid
    regex is dropped, never fatal).
  - `test-model-gate.mjs` grew from 73 to **92 checks**, and the assertion that
    recorded this as a *documented gap* ("a hand-rolled document-store cascade is
    not caught — invariant tests carry it") is now inverted to assert it **is**
    caught. The production shape that started this — `deleteMany({ question_id:
    { $in: ids } })` over a child collection — is the fixture.

## [0.13.1] - 2026-10-16

### Fixed

- **A measurement without a target no longer reads as a verdict.** 0.13.0
  shipped the measurement contract but left the last clause of the requirement
  unimplemented: *where no defensible default exists the output is
  `no-default: needs a declared target`, never a silent pass*. Now:
  - **`perf.target` is the one parameter that ships no default.** A latency or
    throughput target is a product decision; a number ASF invented would be a
    fabricated specification. With nobody having declared one, the parameter
    resolves to `value: null, origin: "no-default"` and the verdict text is the
    literal `no-default: needs a declared target`.
  - **`target` is required on latency and throughput claims.** A claim can carry
    a complete measurement contract — percentile, load, samples, environment,
    lab-or-field — and still be judging nothing. That is now reported, with
    `noDefault` set on the result. A **regression** claim still needs no target:
    it compares two arms rather than a threshold.
  - **The parameter guard keeps its teeth.** The flag that exempts a parameter
    from a band is the same flag that *forbids* it from shipping a general
    default, so a fabricated number cannot smuggle itself in as "no default
    needed". Adding `perf.target` also tripped the existing guard for a missing
    `meaning` field — the guard working as intended on its first real test.
  - `SKILL.md`/06k/06b state the rule where the contract is stated, and the
    config screen shows `no-default` as a first-class origin.

## [0.13.0] - 2026-10-16

### Added

- **The enforcement layer (`lib/gates/`, wired into `/asf`).** Until now every
  ASF gate was a sentence in a markdown file that the agent was asked to
  remember. These are code that runs, each with the two fixtures that make it
  trustworthy.
  - **`defaults.mjs` — three-tier configuration.** Every tunable ships with a
    researched general default (with its source), a documented adaptation band,
    and project calibration allowed **only inside the band**. A value outside
    the band is explicit user configuration and is labelled `override`, never
    silently obeyed. Statistical-validity constants (`bench.significance`,
    `valid.interleavedComparison`, `valid.rawSamplesRetained`,
    `valid.scannedZeroIsPass`) are **non-adaptive** — a project may not
    calibrate its way out of a valid comparison, only make it more patient.
  - **`ledger.mjs` — the evidence ledger.** Entries are derived from observed
    tool traffic (suite runs, health verdicts, browser verification). There is
    **no model-callable way to append an entry**, malformed entries are rejected
    and recorded, and `scanned: 0` is reported as *examined nothing*, never as a
    pass.
  - **`authority.mjs` — gate authority is earned and revocable.**
    `probation → active` requires clean runs **plus at least one caught fixture
    case** (a gate that never fires looks exactly like a gate that works). An
    active gate is demoted automatically when its false-positive rate or
    blocks-per-run exceeds budget. Every transition is append-only, needs a
    reason, and appears in the delivery report. A bypass request is recorded as
    friction, not as evidence that the gate is wrong.
  - **`blockers.mjs` — objective `tool_call` blockers.** Secrets in a tracked
    file, a test subset run without stating its impact area, push without a
    green full suite, destructive git on a dirty tree. Each declares its own
    `failSafe: "block" | "allow"` — a crashing secret check blocks, a crashing
    convenience check does not stop the loop. Every blocker has an autonomous
    exit the agent can take itself; nothing here opens a UI prompt for approval.
  - **`settle.mjs` — the completion gate**, bounded at `MAX_CONTINUATIONS = 2`.
    Past the cap the gaps are **reported**, not blocked forever.
  - **`measurement.mjs` — the measurement contract and the comparison.** A bare
    numeric claim (`p95 < 2s`) is detected and reported as incomplete rather
    than accepted. Regression verdicts use significance **plus effect size**
    with retained raw samples — a naive ±15% threshold missed a real +10%
    regression in 40/40 local trials where this caught 40/40. On a busy machine
    the verdict is `skipped: machine busy`: no verdict, not a pass.
  - **`acceptance.mjs`, `report.mjs`** — perf/security acceptance validation and
    the delivery report, which is **assembled from the ledger and gate verdicts,
    not written**, and shows each DoD item's enforcement class (H / M / S) so a
    self-attested item cannot read as a verified one.
  - **`config.mjs` + `declarations.mjs`** — the config screen (unknown parameters
    refused, non-adaptive ones reported as ignored, out-of-band values clamped
    and labelled) and the obligation declarations, which carry **no consumer
    vocabulary** and are validated against the tests that enforce them.
- **New commands:** `/asf rules [H|M|S|area]`, `/asf config`, `/asf gates`,
  `/asf report [--checklist]`.
- **`obligations.d/aia-asf.json`** — ASF's 17 standing obligations declared for
  the loop guard, `source: "aia-asf"`, carried verbatim by the guard, which
  never interprets it.
- **`references/06k-performance.md`** — the measurement contract, CI-cheap vs
  scheduled placement, load scenarios (including the arrival-model trap),
  escalation, and the ASVS performance/resource overlap.
- **5 new suites, 292 checks** (`test-gates`, `test-performance-gates`,
  `test-config-screen`, `test-delivery-report`, `test-security-mapping`) on a
  shared `test/gate-harness.mjs` that requires **a fire fixture and a near-miss
  fixture for every gate**, and meta-asserts that gate count equals covered
  gate count so a gate cannot be added and quietly left untested.

### Changed

- **06h is now pinned to real ASVS 5.0 numbering** (`v5.0.0_release`, V1–V17).
  The previous chapter table had been written from memory and was wrong. ASF
  does not maintain a second security checklist alongside ASVS, and one
  abuse-case load run now satisfies both the security and performance DoD.
- **06b gains Rules 21–23** (measurement contract, performance placement, gate
  autonomy/negative-controls/budget) and four new DoD lines; the rule count in
  `SKILL.md` is now asserted to *agree* with the heading count rather than to
  equal a hardcoded number, which is what let the drift through.
- **02-research.md gains T1/T2/T3 evidence classes.** Every shipped default
  cites its class and source; a T3 value is never presented as a rule, and "a
  default that cannot explain itself is a guess with better formatting".
- **06d: a delegated verification prompt must carry its measurement contract**,
  and a returned number without one is a claim, not a result.

### Fixed

- **`obligations.d/` was not in `package.json` `files`** — found by clean-room
  verification, not by the suite. An installed ASF would have shipped the
  enforcement code with no declarations for it to load, so `/asf rules` would
  have reported an empty and perfectly green list.
- **`assertSignalsExist` reported false drift in every consumer project.** It
  ran against the installed package, which ships no `test/`, producing one
  bogus failure per obligation. It now says once that the check was skipped
  because this is not the declaring repo — and still fails hard where a `test/`
  directory exists and a named file or assertion is genuinely missing.

## [0.12.2] - 2026-10-14

### Fixed

- **`npm run check` now checks.** It was `tsc --noCheck …` with a compiler that
  was not even resolvable here — `node_modules` is a hand-built symlink tree
  pointing at the global pi install, which `npm install` cannot reconcile, so
  the declared devDependency was never installed. Three layers of nothing:
  no compiler, no checking, and — even when a compiler was found — the host API
  resolved to `any`.
  - `scripts/typecheck.sh` + `tsconfig.typecheck.json`: strict typecheck of
    `index.ts` against the pinned host API, installed into gitignored `.tools/`
    and mapped explicitly. Prints the compiler **and** host-types versions it
    ran, so the gate cannot silently run something other than what is declared.
  - **`lib/code-health/index.d.mts`** — the Code Health gate stays plain ESM on
    purpose, but at the call site its entire surface was `any`. This is the
    boundary: a field added to `runHealth`'s return that is not declared here is
    now a type error rather than a silent pass. Verified by mutation —
    `h.noSuchFieldOnPurpose` → `TS2339`.
  - **devDependencies were stale**: pinned at pi `0.83.0` while the extension
    runs against pi `1.2.0`. Now `1.2.0`, with `typebox` at the `1.3.27` the
    host ships.

### Changed

- **Releases are gated.** `scripts/release.sh` runs the typecheck and the full
  suite before anything is versioned, tagged or published.

## [0.12.1] - 2026-10-14

### Added

- **06j §7 — who enforces the model gate.** The boundary is now written down:
  ASF states the obligation, and the loop guard enforces completion
  **generically**. When a change touches persistence, the model obligations are
  captured as MUST specs (`capture_spec`) and marked `met` with evidence; the
  guard verifies those specs at settle exactly as it verifies any specification
  arriving from any source. What crosses the boundary is the **specification**,
  never the check — the guard does not know what a cascade is and must not have
  to. SKILL.md's Phase 6 model rule now says so directly, and the anti-pattern
  list names the alternative ("asking the loop guard to understand the model").

## [0.12.0] - 2026-10-14

### Added

- **The model gate (P27)** — ASF now inspects the data model, not only the code
  shape. A new mandatory reference, `references/06j-data-model.md`, gives the
  factory what it never had: ownership classes (composition / instance /
  ledger), a required `asf-model:` declaration for every relationship a change
  touches (class, owner, survives-parent), model rules **M1–M8**, the legitimate
  exceptions, and a model Definition of Done. The obligation is the point — a
  declaration that concludes "`CASCADE` is correct here" is a pass; what the gate
  refuses is that nobody was asked.
- **Two objective fitness functions in the Code Health Gate** (`model` level,
  `lib/code-health/model.mjs`): a cascade declaration with no ownership
  declaration, and the rebuild-by-delete signature (delete + re-insert of the
  same table/collection in one window). Built in — no tool to install, so the
  level can never be skipped for a missing dependency — reporting `file:line`
  and the fix, waivable only with a greppable `asf-model-accept: <reason>`.
  Documented as **a tripwire, not an authority**: `scanned: 0` is reported, never
  hidden, and a clean scan is not a pass.
- **Store-agnostic by design.** Declarations are recognised across relational,
  ORM-mapped and document forms, and a project adds its own dialect with
  `model.extraCascadePatterns` rather than waiting for a release. Where a store
  exposes no constraint to inspect, the text says plainly that the invariant
  tests are the only hard gate.
- **Invariant and non-effect tests (06g §3)** — the missing half of the test
  doctrine: survival tests with production-shaped data, non-effect assertions on
  the tables the change does not name, zero-row invariant queries, and two-user
  authorization tests. Every other rule asked *did the thing you wanted happen?*;
  these ask *what must not have happened*.
- **06b Rule 20 — the model is asserted, not assumed**, plus the Rule 10 DoD
  model line, the `/asf verify` checklist item, a **Data & model questions** lens
  in `04-adversarial.md`, a **Data model & ownership** table in the plan
  template, test-first step 6 in `06-implementation.md`, and a **Model drift**
  anti-pattern (extending a destructive persistence pattern because the file
  above does it is drift, not precedent).

### Fixed

- **The rule count in `SKILL.md` can no longer silently disagree with 06b.** The
  suite now derives the count from the `## Rule N —` headings instead of
  asserting a hardcoded number, so adding a rule no longer requires editing two
  test files (and a stale index fails).
- **`docs/research/improvement-proposals.md` header** still claimed "research
  phase only — no ASF code or docs have been changed" long after P23–P26 had
  shipped. It now points at `review-decisions.md` as the authoritative record.

## [0.11.0] - 2026-10-12

### Added

- **Browser verification now follows what a change TOUCHES, and happens in the
  build (06b Rule 9, strengthened in place)**. The rule already existed, but
  every formulation keyed on *"if the deliverable has a UI"* — which an agent
  editing one helper in an existing app reads as not engaged, and "I only
  changed a helper" is exactly the change that reaches a rendered surface
  unnoticed. The trigger is now **can this change reach a rendered surface**,
  with an explicit touched list: pages, routes, components, forms, templates;
  markup, styling, copy, formatting, truncation, empty and error states;
  client-side state, routing and the fetching a view depends on; **an API,
  serializer or query that a UI consumes** (a server-side diff is not exempt
  for living in `src/server/`); auth/permission paths that gate what a user
  sees; user-triggered uploads and downloads.
- **Moved from final verification into the Phase 6 loop.** "Must be verified"
  was Phase 7 language, which makes the gate the first time anyone has looked —
  by then the cause is buried in the whole change. The touched interface is now
  opened in the browser **before committing that change**, while the context is
  warm and the defect is one edit wide (test-first step 5).
- **Same safety inversion as Rule 19**: uncertain whether the change reaches a
  surface? Then it is touched. "It's only a backend change" is not an argument;
  naming the render path it cannot affect is. Uncertainty costs a browser run,
  never a skipped one.
- **Closes a hole Rule 19 opened**: nothing previously stopped a subset run
  from excluding the browser/E2E suite for the surface just changed. For a
  touched surface that suite is now **never excludable** — selection may drop
  unrelated suites, never the one that looks at what you just changed.
- **Evidence, not assertion.** A run is reported with the page/URL opened, the
  action taken and what was asserted — or `n/a: no rendered surface touched,
  because <reason>`. Bare "browser tested" is named as an assertion rather than
  evidence, and an unavailable `pi-aia-browser` must be said out loud instead of
  reading as a passed check. Backed by the Rule 10 checkbox, the `/asf verify`
  checklist item, four new anti-patterns, and `test/test-browser-touched.mjs`
  (42 assertions, 6 negative controls). Rule 9 keeps its number, its five steps
  and its silent-async paragraph — strengthened, not duplicated.

## [0.10.0] - 2026-10-07

### Added

- **06b Rule 19 — test selection for the inner loop (P25)**. Rule 10's
  "full test suite green (not a subset)" is a **gate**, and it stays exactly
  where it is: the run before a commit is always the full suite. What Rule 19
  relaxes is only the **intermediate red-green-refactor runs inside a single
  change** — the runs on code you are still editing. The rule is the user's
  own: **run a partial, impact-area subset only when certain that is
  warranted; if not certain, or the blast radius *might* be larger, always run
  full. Default is full** — so the burden of proof sits on the subset, and the
  cheap direction to be wrong in is also the safe one.
- **"Certain" is evidence, not a feeling.** A subset run must be preceded by a
  stated **impact statement** — changed files, tests selected, and *why nothing
  else can be reached*. Restating the file list does not qualify; no statement
  means not certain, which means full. On top of that, a **mechanical
  disqualifier list** removes judgment wherever "might be bigger" is the
  honest answer: shared/global state, dependencies and lockfile, build/test/CI
  config or the harness itself, schema/migrations/shared fixtures, public
  contracts and shared types, cross-module refactor/rename/move/delete — and
  **any changed file with no entry in the test-impact map**, so a project that
  has not built a map always runs full. The relaxation only ever applies where
  the mapping is known, never where it is guessed.
- **Deliberately not built**: the drafted coin toss and `iteration % N`
  counter are gone. An LLM cannot roll a dice — asked to flip one it picks the
  answer that skips the slow run — and an undefined iteration boundary means
  the trigger silently never fires. A subset streak also cannot survive a
  commit, because every commit is full-gated. Below a measured ~90s full-suite
  runtime the assessment is skipped entirely and the suite is just run.
- **Wired in**: `06b-testing-qa.md` Rule 10's checkbox now says in place that
  the gate never moves; `06-implementation.md` test-first steps 3 and 4 name
  the loop/gate split; `SKILL.md` gains two anti-patterns (a subset run with no
  impact statement, and a subset run passed off as the Rule 10 checkbox) and
  the index count moves to 19 rules. Covered by
  `test/test-test-selection.mjs` (26 assertions), which includes a regression
  guard that **fails if the coin-toss or iteration-counter design ever returns**.

## [0.9.2] - 2026-10-04

### Added

- **`references/06i-completeness.md` — the completeness standard (P24, mandatory)**.
  Newer models increasingly ship work that *looks* finished: a function whose body is
  a placeholder, a screen populated with invented records, a report citing a limit
  nobody checked. Green tests do not catch it — the tests get written against the
  stub, so the fake looks verified. The reference states the core rule (**ship the
  real thing, or say out loud what is not real**), the banned patterns (`TODO` /
  `not implemented` / `coming soon` on a user-reachable path, fake or seed data the
  user reads as their own, invented facts stated as checked, silently deferred scope,
  built-but-never-wired, mocks in production paths, config nothing sets), the
  exceptions that must be **declared, never silent** (the user asked for a scaffold,
  an approved deferral, a fake at a test seam), how to prove it is real (exercise each
  part through the entry point the user will use and name that action in the delivery
  report), and a completeness DoD.
- **Wired in, not bolted on**: Phase 6 gains a mandatory read block and discipline
  rule 11; Phase 7 gains the completeness DoD check; the anti-pattern list gains six
  entries (stub wearing a name, fake data the user reads as their own, invented facts
  stated as fact, quietly deferred scope, stub presented as complete because its own
  tests pass, unexplained deliberate stub); the reference index marks 06i as always
  read. Proportionality holds — small work reads only the core rule and the DoD.

### Requires

- The enforcement side lives in **pi-vigilant 0.6.2**, which scans the files a
  session changed for stub / fabricated-data / fact-hole markers at final
  verification and puts the `file:line` evidence in front of the agent. The rule and
  the check are the same proposal: the agent must finish the work or state the
  discrepancy.


## [0.9.1] - 2026-10-03

### Fixed

- **Extension no longer loads on pi >= 1.0** — pi 1.0 changed the command
  contract to `registerCommand(name, { description, handler })`, where
  `handler(args: string, ctx)` returns void and user-visible output goes
  through `ctx.ui.notify()`. `/asf` and `/asf-approve` were registered as bare
  functions taking `(args: string[], ctx)` and returning their text, so pi
  1.0.1 refused to load the package at all:
  `Command "/asf" registered by extension ".../index.ts" must define handler()`.
  Both commands now use the options-object form; the argument string is split
  into subcommand + flags and every gate report is notified. Subcommand
  behaviour is unchanged.

### Tests

- The test harness mock now enforces the real `RegisteredCommand` contract
  (it accepted any shape before, which is how this shipped) and exposes
  `invokeCommand()`, which calls a registered command exactly as pi does and
  captures `ctx.ui.notify()` output.
- New `test/test-command-contract.mjs` (16 checks): contract enforcement with a
  negative control, arg-as-string parsing, case-insensitive subcommands,
  void return, notify-based output, no bare-function registration left.


## [0.9.0] - 2026-09-27

### Added

- **Indexed reference structure (P-index)** — SKILL.md now carries a reference
  index table (file → covers → read when) so the agent finds the right
  section on demand; each reference is a small single-concern doc with a
  "Read when / Skip when" header. Default context cost is unchanged (SKILL.md
  only); large work reads at most the 3–4 references its phase needs.
- **`references/06f-stability.md`** — stability patterns (timeouts, bounded
  retries, circuit breaker, bulkheads, bounded result sets, fail fast,
  graceful shutdown) + error-handling design rules + observability DoD for
  service-like deliverables (health endpoint, structured logs, correlation
  IDs, runbook) (P06, P08, P07).
- **`references/06g-test-design.md`** — risk-based test derivation
  (likelihood × impact, technique selection table) + determinism rules
  (P10, P11).
- **`references/06h-security.md`** — one-page threat model (trust boundaries,
  STRIDE, registry), ASVS 5.0-based DoD with chapter/level selection,
  supply-chain & secrets gates, and the agent/tool-surface chapter (P15,
  P16, P17, P18).

### Changed

- **05-plan.md** — PLAN.md template extended: Decisions (ADRs, P01), Quality
  requirements (ISO 25010 scenarios, P02), Architecture evaluation (ATAM-lite,
  P03), optional Views (P04).
- **04-adversarial.md** — added security lens (threat model) and architecture
  lens (ATAM-lite) applied on every review (P15, P03).
- **06c-code-quality.md** — deep-module principle (interface simpler than
  implementation, P05), code smells in 5 groups (P09), characterization
  baseline for refactoring untested code (P13).
- **06b-testing-qa.md** — Rule 16 determinism (flaky = defect, P11), Rule 17
  delivery report fields incl. not-tested register (P14), Rule 18
  escaped-defect feedback loop (P21); DoD extended with delivery-report,
  stability and security lines.
- **06e-code-health.md** — report-only depth metric (P05) + supply-chain
  (npm audit/osv-scanner) and secrets (gitleaks) gates with config (P17).
- **07-release.md** — verify-first now re-runs the dependency/security check
  before every release (P17 RV loop).
- **06-implementation.md** — drift self-correction: named anti-patterns,
  stop → revert → self-correct → continue, escalate only on genuine
  conflicts (P20).
- **06d-delegation.md** — Part 3: independent reviewer gate for large work
  (fresh-context hostile reviewer, blockers vs suggestions, autonomous
  triage, human only on genuine conflicts) (P19).
- **SKILL.md** — ASF self-modification policy: facts may be updated in the
  same change; standards change only via the review process (P22).
- **test/test-improvements.mjs** — new suite (84 checks) verifying the
  indexed structure and that each proposal landed in its designated file.
- **Configurable release policy (P23)** — `.asf-release.json` at the project
  root (optional): `when` (user-stated conditional rules: "when X is true,
  publish automatically; when Y is true, do this…") and `how` (user-stated
  instructions with all technical details that the ASF drives with judgment,
  not a deterministic command list). Default with no file = current stance
  (always send for final review); user definition takes precedence.
  **Test on publish / in production, if applicable — always** (package →
  clean-room registry install + observable end state; deployed app → prod
  smoke test). The ASF's own release procedure is codified via the same
  mechanism (repo `.asf-release.json`).

## [0.8.1] - 2026-09-25

### Added

- (describe changes for 0.8.1)


## [0.8.0] - 2026-09-12

### Added

- **Code Health Gate** — objective, configurable measurement of code
  convolution at three levels, replacing pure judgment with measurable
  thresholds (references/06e-code-health.md):
  - **function** — cyclomatic complexity (20), cognitive complexity (15),
    lines per function (50), nesting depth (4), parameters (3) via eslint
    (`--rule` flags, not the project's own config);
  - **module** — lines per file (300) via eslint, duplication (5% / min 5
    lines / 50 tokens) via jscpd, circular imports (any cycle = fail) via
    madge;
  - **architecture** — optional dependency-cruiser rules (off by default).
- **`/asf health`** — runs the gate and prints a `metric | value | threshold |
  status` table with a verdict; exit 1 when the gate fails. Flags: `--diff`
  (regression trend vs the committed baseline), `--update-baseline`, `--json`.
- **Gate 8 in `/asf verify`** — large work runs the code health gate after the
  M1 traceability matrix; block mode fails the gate, warn mode reports only.
- **`.asf-code-health.json`** — optional project config, merged over defaults
  (defaults ← project ← CLI). On/off at every level: master `enabled`,
  per-level, per-metric. `gate.mode` block|warn, `gate.scope` large|all|off,
  `missingTool` warn|fail, `toolTimeoutSeconds` (default 120s, Rule 15).
- **Committed baseline** — `.asf-code-health-baseline.json` is committed so the
  trend survives across machines/CI; `--diff` flags any regression.
- **Test suites** — `test/test-code-health.mjs` (73 checks: pure modules,
  pipeline triggers + non-triggers with fake tools, real-exec timeout,
  real-eslint integration) and `test/test-code-health-ext.mjs` (12 checks
  through the real `/asf health` + `/asf verify` command handlers with a real
  eslint).

## [0.7.0] - 2026-09-11

### Added

- **Absolute wait ceiling — 30 minutes (06b Rule 15)** — no single wait may
  exceed 30 min (1800s). Addresses the reported failure where tool calls waited
  forever for the system to react and the whole process kept hanging,
  especially during testing. The ceiling is a **backstop, never a substitute**
  for a real per-call bound: a 30s suite still gets ~120s, and a lone 1800s
  bound is treated as not having bounded anything. Hitting the ceiling means
  kill the wait and diagnose the blocking cause — it is a bug signal, not
  patience.
- **Explicit per-run waiver** — the ceiling can be disabled, but only
  deliberately and only for a **single run**: stated before running, with the
  reason and a finite larger bound. A waiver does not persist, does not carry
  over, and resets immediately afterwards. Blanket/standing waivers are
  disallowed; repeated need for waivers is a setup defect to report (Rule 11).
- **"Every waiting call carries its own explicit timeout"** — makes the rule
  cover *every* tool call or command that waits on the system, not just the
  obviously slow ones, with testing called out as the highest-risk area
  (harnesses, servers, daemons, browsers hanging on ports, locks and prompts).
  Bounds are sized from the stated expected duration with headroom.
- New Definition-of-Done item for the ceiling; ceiling applies at **both
  scales** (small work is not exempt).
- **Durable test suite in `test/`** — the ASF suites were previously only in
  `/tmp` and would have been lost. `npm test` now runs 67 checks (timeouts,
  ceiling, delegation, docs-rule) against the real shipped skill text;
  `test/setup.sh` prepares the jiti harness against the installed pi.

### Verification

- 67/67 checks pass (`npm test`): 13 timeouts, 17 ceiling, 19 delegation,
  18 docs-rule.
- The 17 new ceiling checks were confirmed to **fail 11/17 on the pre-change
  text** before implementation, proving they assert real content.
- SKILL.md YAML frontmatter re-verified as parsing (pi silently skips skills
  with malformed frontmatter).


## [0.6.1] - 2026-08-28

### Removed

- **pi-intercom is no longer a dependency** (reversal of the 0.4.0 addition,
  per user decision): removed from `dependencies` in package.json and from the
  runtime dependency check — `/asf` summary is back to the four required
  packages (pi-vigilant, pi-smart-web-search, pi-smart-fetch, pi-aia-browser).
  ASF works fully without it; intercom is documented as an **optional** tool
  (`pi install npm:pi-intercom`) in the README, 06d-delegation.md, and the
  SKILL.md delegation note. Rationale: a hard dependency broke the workspace
  installer's `--skip=pi-intercom` semantics (npm pulled it back in via
  pi-aia-asf), and delegation is a nice-to-have, not a requirement.

## [0.6.0] - 2026-08-28

### Added

- **06c Rule 9 — Documents are code: refactor them when they outgrow
  editability** (new mandatory rule, Ai Applied tenet): when a document starts
  causing writing/editing issues (edit-tool matching failures, truncation,
  multi-topic bloat, every change touching the same big file), it is time to
  refactor it — split by topic, keep the parent as an index/map, keep
  cross-references exact, preserve content (Rule 8 applied to documents),
  verify, and refactor before it hurts rather than after.
- **Wiring**: 06c "Where this applies" (Phase 6 + Phase 7), 06c anti-patterns
  (letting a document grow until edits break; wall-of-prose SKILL.md), SKILL.md
  Phase 6 note, Phase 7 modularity DoD (documents too), SKILL.md anti-pattern,
  and the reference index (now 9 rules).

## [0.5.0] - 2026-08-28

### Added

- **06b Rule 15 — bound every long-running operation** (new mandatory QA rule):
  state the expected duration before running any long command (package installs,
  platform/test-harness startup, browser/daemon launches, full suites); ALWAYS
  wrap it in a hard timeout (`timeout N …` / tool timeout parameter); exceeding
  the bound is a bug signal — kill and diagnose the root cause; if the bound
  proves too short, raise it deliberately with a reason. Rooted in the user
  report of package hangs lasting up to 50,000 seconds with the agent waiting
  idle. Added to the Rule 10 definition-of-done checklist.
- **SKILL.md discipline rule 6 — Bounded waits — ALWAYS** (renumbered rules
  7–10): the same rule at the phase level, with the 50,000-second hang as the
  anti-example.
- **07-release.md**: "Verify first" step now requires every long-running
  verification command to have run under an explicit timeout with a stated
  expected duration.

## [0.4.0] - 2026-08-28

### Added

- **Delegation reference `references/06d-delegation.md`** — codifies the two
  delegation mechanisms available to ASF work: **intercom** (message another
  live pi session that owns relevant context — `list` first, say what you want
  back, treat peer findings as evidence, not proof) and **subagents** (spawn an
  isolated `pi -p` process when the OUTCOME matters more than the trace — scoped
  codebase research, independent parallel fixes, fresh-perspective review).
- **Two verified hard limits, backed by experiment** (both documented as
  anti-patterns in SKILL.md):
  - **Never run parallel subagents against the same file.** Tested: 4 concurrent
    whole-file writers left only 1 of 4 edits, silently, all exiting 0 — the
    no-mutual-dependencies rule is enforced by physics, not preference.
  - **Exit code 0 does not mean success.** Tested: a subagent asked to read a
    non-existent file exited 0. Always validate the returned output.
- **`pi-intercom` as a declared dependency** (`dependencies` in package.json,
  `^0.10.1`) — `npm install` pulls it in. Intentionally **not bundled**: a
  bundled copy conflicts with a top-level `pi install npm:pi-intercom`
  (tool-name collision, verified), so the tool must be installed top-level to
  register. Added to the ASF runtime dependency check (`/asf` summary now lists
  all five: pi-vigilant, pi-smart-web-search, pi-smart-fetch, pi-aia-browser,
  pi-intercom).
- **SKILL.md wiring** — Phase 6 delegation note, reference index entry, and
  three new anti-patterns (same-file parallel subagents, trusting subagent exit
  codes/self-reports, re-deriving context another live session already has).

### Changed

- README dependencies table now lists pi-intercom with the not-bundled rationale.

## [0.3.0] - 2026-08-25

### Added

- **Mechanical spec-to-code traceability gate in `/asf verify`** (large work
  only) — reads the shared spec-memory task and validates every `met` spec's
  trace (`outcome → codePath → testFile + assertion`); `testFile` must exist
  and `assertion` must appear in it. Outputs a traceability matrix with
  PASS/FAIL rows and refuses delivery on FAIL (`GATE NOT PASSED`). Small work
  keeps the checklist only — no mechanical gate (scale proportionality).
- **New QA rules (06b-testing-qa.md)**: Rule 12 — test the operator-facing
  outcome, not the machinery (Betamaxx dead-code failure); Rule 13 — nothing
  is delivered until consumed by a surface (UI or API); Rule 14 — every
  feature spec ships an E2E behavioral test through the real entry point.
  Rule 9 extended for silent async paths; Rule 10 DoD extended (traceability,
  consumed-by-surface, E2E).
- **M4 — challenge approved designs during implementation** (06-implementation.md):
  product tension → resolve with the user before implementing.
- **M5 — the delivery log is a claim; the code is the evidence**: trace the
  actual code path before marking anything delivered.
- **M6 — external planning docs are specs, not ground truth** (01-intake.md +
  SKILL.md Phase 1/3): locate IMPROVEMENT-PLAN.md / PLAN.md / requirements
  docs / delivery logs / tickets, ingest every actionable item as a captured
  spec; their ✅ markers are claims, not evidence.
- **Adversarial traceability lens** (04-adversarial.md): operator-facing
  outcome + code path + where dead wiring hides.
- **PLAN.md traceability matrix** (05-plan.md): spec → outcome → code path →
  test.
- DoD checklist extended; anti-patterns added (delivery-log trust, machinery
  without a surface, unit tests as wiring proof, external plan checkmarks,
  literal implementation of tension-creating specs).

### Changed

- `/asf verify` output now shows scale and, for large work, the mechanical
  traceability matrix before the DoD checklist.

### Requires

- pi-vigilant 0.1.3+ (provides the `trace` field on specs).


## [0.2.2] - 2026-08-14

### Added

- **Mandatory modularity & maintainability standard** (`references/06c-code-quality.md`) — 8 rules distilled from Bruno's Tunnel-project philosophy (small well-readable modules plugged in where needed; one authoritative implementation for shared functionality with a single escalation path; no hardcoding — config-driven; fully testable outside the host then integrated verbatim, same modules in tests and production; refactor what is too complex to understand; layered with clear one-way boundaries and an architecture writeup; full I/O debug logging with replay of stored data; nothing may break existing functionality) and reinforced by external research (SSOT, testability as design property, ports-and-adapters seams).

### Changed

- **Phase 6** now requires reading `06c-code-quality.md` before structuring code; **Phase 7** verification includes the modularity DoD (no duplicated shared logic, no hardcoded config values, standalone-tested modules, architecture writeup, existing functionality green).
- **Phase 4** adversarial analysis gains a code-quality lens (duplication, single escalation path, standalone testability, hardcoded values); **Phase 5** PLAN.md architecture section now requires the SSOT map, layering rules, and standalone-testability notes.
- Anti-patterns extended: god-objects, copy-pasted shared logic, test copies of modules, hardcoding, per-caller escalation logic, untestable-standalone modules, behavior-breaking refactors.


## [0.2.1] - 2026-08-14

### Added

- **Scale-based flow.** Every ASF activation is now classified **small** or **large**:
  - **Small** (small features, minor bugfixes, small refactors) — runs **automatically**: no intake question barrage (at most one clarifying question), no research, no adversarial gate, no PLAN.md, no approval. State the intent, capture specs silently, implement test-first, verify, deliver.
  - **Large** (new projects, significant features, major bugfixes, architectural refactors) — full gated flow unchanged: intake → research → specs → adversarial → PLAN.md → **explicit approval** → implementation → verification.
  - When in doubt, **default to small and start working**; re-classify upward if the work grows.
- **`/asf small`** command — starts a small session directly in implementation phase. `/asf status` now shows the scale (`automatic — no gates` for small).

### Changed

- All gates (1–5) explicitly apply to large work only; Phase 2 (research) and Phase 5 (PLAN.md + approval) are skipped entirely for small work.
- References `01-intake`, `04-adversarial`, `05-plan` annotated with the scale rule.
- Anti-patterns: no question barrage or PLAN.md/approval demand for small work; when in doubt, start small.


## [0.2.0] - 2026-08-14

### Added

- **Mandatory testing & QA standard** (`references/06b-testing-qa.md`) — 11 rules distilled from real shipped-broken failures: test the packaged artifact (not just source), assert the observable end state, probe for silent failures, read the actual error before editing, clean-room verification against stale caches, a regression test for every bug fixed, test triggers *and* non-triggers, state-machine deadlock tests, mandatory browser testing for web surfaces, a definition-of-done checklist, and honest reporting.
- **`/asf verify`** — an explicit definition-of-done gate that itemises the 9 required checks, points at the QA standard, and forbids self-certifying unverifiable specs.

### Changed

- Phase 6 and Phase 7 of the skill now require the QA standard; Phase 7 additionally requires verifying the packaged artifact and the observable end state, plus honest reporting of skipped or inconclusive checks.
- Anti-patterns extended: shipping without inspecting the packaged file list, treating "no error" as success, verifying against a stale install, guessing before reading the error, fixing without a regression test, and claiming assumed verification.

### Fixed

- **`/asf-approve` rubber-stamped Gate 5.** It marked the plan approved even when no `PLAN.md` existed — approving a plan the user had never seen. It now refuses unless `PLAN.md` is present, and advances the phase to implementation on success.


## [0.1.1] - 2026-08-14

### Fixed

- **The `aia-asf` skill was never loaded.** Its YAML frontmatter description contained an unquoted colon (`The flow is: classify the work...`); a colon followed by a space starts a mapping in YAML, so the frontmatter failed to parse and pi silently skipped the skill. The extension's `/asf` commands worked, but the workflow skill itself was unavailable. The description is now a YAML block scalar with no inline colon.


### Added

- Initial release of the Ai Applied Agentic Software Factory.
- ASF skill with 8 gated phases: classify, intake, research, specs, adversarial analysis, plan (approval gate), test-first implementation, verification & delivery.
- Per-phase reference guides (intake question bank, research playbook, adversarial checklist, PLAN.md template, implementation discipline, release workflow).
- `/asf` commands: new, feature, bugfix, refactor, status, approve, abort.
- Per-project phase state tracking in `~/.pi/agent/skills/aia-asf/projects/<project>/state.json`.
- Dependency checks at startup and on `/asf` (pi-vigilant, pi-smart-web-search, pi-smart-fetch, pi-aia-browser).
- Specs shared with pi-vigilant via `capture_spec` / `get_task_specs` / `update_spec_status`.
- Strict codebase isolation rule (project never touches other repos unless the user says so).
- Mandatory pi-aia-browser testing for any web interface.
- Release workflow: prepare version + CHANGELOG + tag + push; publishing always requires explicit user approval.
