> For AI agents: the complete documentation index is available at https://docs.phax.run/llms.txt, the full documentation bundle is available at https://docs.phax.run/llms-full.txt.

# phax 0.28.0 \{#phax-0280}

Changes since 0.27.0: [v0.27.0...v0.28.0](https://github.com/lbdremy/phax/compare/v0.27.0...v0.28.0).

## Highlights \{#highlights}

- **Test strategy as data, and the plan overview** — The overview shows what the plan says, read without a model, and it never writes or starts anything.
- **Artifact decide from verdicts** — The person decides and phax checks the transcription: what the file says is applied as given, nothing it does not name moves, and nothing is approved.

## Upgrading \{#upgrading}

- **A spec or plan option named `other`.** `other` is now a reserved id, implicit on every question. A live spec or plan that lists an option, or a recommendation, with the id `other` is refused: an authored answer exits 5, `phax artifact approve` and `decide` on the artifact exit 12, and `phax artifact status` reports a §9 that does not parse. Rename that option (in the sidecar and in the Markdown alike) before upgrading. A `Decision: other — …` line, and a verdict that chooses `other`, are accepted.
- **Plan sidecars: `testStrategy` becomes a list of test lines.** Run `phax artifact migrate-documents` once in each repository with plans under `docs/`. A sidecar whose every test strategy follows the line format is lifted and committed. One holding prose is refused, naming the phase and the line: rewrite it in the line format in the sidecar and in plan.md alike, or re-author the plan, then run the command again. Until then, `phax artifact approve`, `reopen` and `decide` refuse and name the command. Archived plans are not touched and still read.
- **Hand-written plans.** Write each `### Test strategy` entry in the line format: `<guarantee> — <size> · <technique> · <oracle> · <loop>[ · <criterion>]`. `phax plans lint` reports a departing line as a `tests` error. `phax run`, `phax plans overlap`, `phax adjust-plan` and `phax plans status` refuse such a plan before any model call, naming the line and `phax plans lint`, and `phax plans overview` exits 1. A phase with no section, or with `- (none)`, has no line.
- **Runs in flight.** A `phax-plan.json` extracted by an older phax is refused by `phax resume` and `phax validate --plan`. Finish the run with the release that started it, or archive it with `phax archive <run> --force` and run its plan again. `phax ls` and `phax session-info` still show it.
- **New `tests` warnings in `phax plans lint`.** Nothing to do. An uncovered acceptance criterion, or a source spec without a readable sidecar, is a warning that never fails lint or refuses a run.

## What's new \{#whats-new}

### Test strategy as data, and the plan overview \{#test-strategy-as-data-and-the-plan-overview}

> The overview shows what the plan says, read without a model, and it never writes or starts anything.

- A plan's test strategy is a list of test lines a tool can read, each with its size, technique, oracle, loop and the criterion it proves. `plan sidecar docs/plans/<stamp>-<slug>-plan.json, phase testStrategy`

  Now:

  ```text
  "testStrategy": [
    {
      "guarantee": "every month boundary rounds as the table says",
      "size": "small",
      "technique": "table",
      "oracle": "boundary table",
      "loop": "oracle",
      "criterion": "ac-month-boundary"
    }
  ]
  ```

  :::details Before

  ```text
  "testStrategy": "- every month boundary rounds as the table says — small · table · boundary table · oracle · ac-month-boundary"
  ```

  :::

- The extracted plan carries each phase's test lines, so scripts and later commands can see what each phase tests. `phax-plan.json, phase test lines`

  Now:

  ```text
  {
    "id": "phase-01",
    "title": "Export decision rules",
    …
    "testLines": [
      {
        "guarantee": "every month boundary rounds as the table says",
        "size": "small",
        "technique": "table",
        "oracle": "boundary table",
        "loop": "oracle",
        "criterion": "ac-month-boundary"
      }
    ]
  }
  ```

  :::details Before

  ```text
  A phase in phax-plan.json had no test lines: the plan's Test strategy section was never extracted.
  ```

  :::

- You write each test of a phase as one line (the guarantee, then size, technique, oracle, loop and an optional criterion), and phax reads it. `plan.md ### Test strategy section`

  Now:

  ```text
  ### Test strategy

  - a failed write leaves no partial export — small · example · fake fs state · in-phase · ac-atomic
  - the fake disk and the real one agree on a rename — medium · contract · shared suite · in-phase
  ```

  :::details Before

  ```text
  ### Test strategy

  Write the exporter first, then test that a failed write leaves no partial export.
  ```

  :::

- Linting a plan now flags test lines that break the format, misuse the oracle loop or cite a criterion the spec lacks, and warns about spec criteria no test proves. `phax plans lint <plan> — new test line findings`

  Now:

  ```text
  $ phax plans lint docs/plans/2610240900-invoice-export-plan.md
  docs/plans/2610240900-invoice-export-plan.md: 3 errors, 1 warning
  error    tests      phase-01  test line 2 departs from the format: "add unit tests for the exporter"
  error    tests      phase-02  test line 2 names criterion "ac-retry", which invoice-export does not have
  error    tests      phase-02  test line 3 is in the oracle loop, but phase-02 is not an oracle phase
  warning  tests      -         criterion "ac-empty-month" of invoice-export is named by no test line
  ```

  :::details Before

  ```text
  $ phax plans lint docs/plans/2610240900-invoice-export-plan.md
  docs/plans/2610240900-invoice-export-plan.md: no findings
  ```

  :::

- You can see a whole plan at a glance before running it: each phase's model, oracle role, files and tests. `phax plans overview <plan> [--json]`

  Now:

  ```text
  phax plans overview <plan> [--json]

  $ phax plans overview docs/plans/2610240900-invoice-export-plan.md
  invoice-export — Monthly invoice export  (Draft · source spec invoice-export)
  architecture: parts from the configured provider
  …

  $ phax plans overview plan.md
  plan.md: the plan does not parse — run phax plans lint plan.md
  ```

- The overview lists each phase in turn with its model, its oracle role, its files grouped by architecture part and its test lines. `phax plans overview — rendered layout`

  Now:

  ```text
  $ phax plans overview docs/plans/2610240900-invoice-export-plan.md
  invoice-export — Monthly invoice export  (Draft · source spec invoice-export)
  architecture: parts from the configured provider

  phase-01 — Export decision rules  claude-sonnet-5-5 · medium · headless
    oracle role  oracle phase, discharged by phase-02
    domain       src/domain/invoice/export.ts
    tests        tests/unit/invoiceExport.test.ts
    - every month boundary rounds as the table says — small · table · boundary table · oracle · ac-month-boundary

  phase-02 — Export use case  claude-sonnet-5-5 · high · headless
    oracle role  discharges phase-01
    app          src/app/exportInvoices.ts
    (no part)    scripts/export-fixture.sh
    - a failed write leaves no partial export — small · example · fake fs state · in-phase · ac-atomic

  acceptance criteria of invoice-export
    ac-month-boundary  phase-01 oracle
    ac-atomic          phase-02 in-phase
    ac-empty-month     (no test line)
    uncovered          ac-empty-month
  ```

- A script or review tool can read the plan overview as JSON. `phax plans overview --json`

  Now:

  ```text
  $ phax plans overview docs/plans/2610240900-invoice-export-plan.md --json
  {
    "plan": "docs/plans/2610240900-invoice-export-plan.md",
    "name": "invoice-export",
    "sourceSpec": { "path": "docs/specs/2610231100-invoice-export.md", "slug": "invoice-export" },
    "architecture": { "kind": "parts" },
    "phases": [
      {
        "id": "phase-01",
        "oracleRole": { "kind": "oracle", "dischargedBy": "phase-02" },
        "placement": [{ "part": "domain", "files": ["src/domain/invoice/export.ts"] }, …],
        "testLines": [{ "guarantee": "every month boundary rounds as the table says", "criterion": "ac-month-boundary", … }],
        …
      },
      …
    ],
    "coverage": { "kind": "criteria", "uncovered": ["ac-empty-month"], … },
    …
  }
  ```

- Migrating documents refuses a plan whose test strategy is still prose, naming the line and how to rewrite it in the line format. `phax artifact migrate-documents — refusing a prose test strategy`

  Now:

  ```text
  $ phax artifact migrate-documents
  migrate refused: docs/plans/2610200900-invoice-export-plan.md phase-01 testStrategy line 1 departs from the test line format: "Write the exporter tests first, then cover the month boundaries."
    Rewrite each phase's test strategy in the line format, in the sidecar and plan.md alike, or re-author the plan, then rerun.
  ```

- Agents writing plans with the phax-planning skill write test strategies in the line format that lint checks and the overview shows. `.claude/skills/phax-planning/SKILL.md`

  Now:

  ```text
  ## Test strategy

  A phase's `### Test strategy` is data. phax reads each line into a test line (`testLines` in
  `phax-plan.json`), `phax plans lint` checks the lines, and `phax plans overview <plan>` shows them
  phase by phase with the coverage of the source spec's criteria. One line per guarantee the phase
  adds or changes, each placed on four coordinates.
  …
  - `testStrategy`: an array of test lines `{ guarantee, size, technique, oracle, loop, criterion }`,
    every key required and `criterion` null when the line names none; `[]` for a phase that tests
    nothing. See [Test strategy](#test-strategy).
  ```

  :::details Before

  ```text
  ## Test strategy (informational)

  A phase's `### Test strategy` is not extracted, but it is held to a strict shape: one line
  per guarantee the phase adds or changes, each placed on four coordinates.
  …
  - `testStrategy`, `verification`, `expectedHandoff`: strings. `testStrategy` holds the
    guarantee lines of [Test strategy](#test-strategy-informational), one `- ` line each,
    separated by newlines.
  ```

  :::

### Artifact decide from verdicts \{#artifact-decide-from-verdicts}

Transcribing decisions already made.

> The person decides and phax checks the transcription: what the file says is applied as given, nothing it does not name moves, and nothing is approved.

- You can record decisions you have already made on a spec's or plan's questions from a file, without an interactive session. `phax artifact decide --verdicts`

  Now:

  ```text
  phax artifact decide <path> --verdicts <file|-> --by operator [--doctrine <file>] [--model <model>] [--effort <effort>] [--json]

  $ phax artifact decide docs/specs/2610201015-log-rotation.md --verdicts verdicts.json --by operator
  decide spec log-rotation — verdicts from verdicts.json: q-keep, q-scope — headless, new session
  model      claude-sonnet-5-5 / medium (built-in default) on claude-code
  q-keep     keep-20   departs from keep-10 — revised: 5.1, AC-1
             why: Support asks for a fortnight of runs.
  q-scope    per-run   (recommended)
             why: A report names its run, so a log is kept with its run.
  q-names    open      (not in the verdicts file)
  diff --git a/docs/specs/2610201015-log-rotation.md b/docs/specs/2610201015-log-rotation.md
  …
  -THE system SHALL keep the ten most recent logs.
  +THE system SHALL keep the twenty most recent logs.
  …
  commit     e8331a3 — docs(specs): decide log-rotation q-keep, q-scope
  status     Draft — read the verdicts and the diff above, then `phax artifact approve docs/specs/2610201015-log-rotation.md --by operator`
  record     decision/2610201015-log-rotation/01

  ✗ Decide refused: verdicts.json chooses keep-30 for q-keep — its options: keep-10, keep-20, or other
  ```

  :::details Before

  ```text
  phax artifact decide <path> [--question <id>] [--doctrine <file>] [--model <model>] [--effort <effort>]

  $ phax artifact decide docs/specs/2610201015-log-rotation.md < /dev/null
  ✗ Decide refused: no terminal — phax artifact decide is interactive only
  ```

  :::

- You write your decisions in one small file that names, for each question, the option you chose and an optional note. `verdicts file (given as --verdicts <file|->)`

  Now:

  ```text
  {
    "$schema": "https://docs.phax.run/schemas/decide-verdicts/0.28.0.json",
    "artifact": "docs/specs/2610201015-log-rotation.md",
    "verdicts": [
      { "question": "q-keep",  "chosen": "keep-20", "note": "Support asks for a fortnight of runs." },
      { "question": "q-scope", "chosen": "per-run", "note": null },
      { "question": "q-names", "chosen": "other",   "note": "Name each log by its date and its phase." }
    ]
  }
  ```

- You can choose none of a question's options and say what you want instead, and the spec shows that choice and why. `docs/specs/<stamp>-<slug>.json a verdict choosing other, and the rendered §9`

  Now:

  ```text
  "verdict": {
    "kind": "decided",
    "chosen": "other",
    "abandoned": ["by-date", "by-run"],
    "why": "Name each log by its date and its phase.",
    …
  }

  ### q-names — How are rotated logs named?

  - by-date — By date — abandons: a name that tells the run apart
  - by-run — By run — abandons: a name that sorts by date

  Recommendation: by-date — Dates sort in a listing and need no lookup.

  Decision: other — by operator (Ada Lovelace), 2026-10-26
  Why: Name each log by its date and its phase. …
  ```

  :::details Before

  ```text
  A verdict could only choose one of the question's listed options.
  ```

  :::

- You can print the verdicts file's JSON Schema to check or generate a verdicts file. `phax artifact schema verdicts`

  Now:

  ```text
  $ phax artifact schema verdicts
  {
    "$schema": "http://json-schema.org/draft-07/schema#",
    …
    "type": "object",
    "required": [
      "$schema",
      "artifact",
      "verdicts"
    ],
    …
    "title": "phax decide verdicts"
  }
  ```

  :::details Before

  ```text
  $ phax artifact schema verdicts
  error: command-argument value 'verdicts' is invalid for argument 'kind'. Allowed choices are spec, plan.
  ```

  :::

- A script can read what decide recorded: each verdict, whether it departs from the recommendation, and which sections changed. `phax artifact decide --verdicts --json`

  Now:

  ```text
  $ phax artifact decide docs/specs/2610201015-log-rotation.md --verdicts - --by operator --json < verdicts.json
  {
    "artifact": "docs/specs/2610201015-log-rotation.md",
    "status": "Draft",
    "verdicts": [
      { "question": "q-keep", "chosen": "keep-20", "recommendation": "keep-10", "departs": true,
        "why": "Support asks for a fortnight of runs.", "revised": ["5.1", "AC-1"], … },
      { "question": "q-scope", "chosen": "per-run", "recommendation": "per-run", "departs": false, … }
    ],
    "open": ["q-names"],
    "diff": "diff --git a/docs/specs/2610201015-log-rotation.md …",
    "commit": { "hash": "e8331a3d…", "subject": "docs(specs): decide log-rotation q-keep, q-scope" },
    "decisionId": "2610201015-log-rotation/01",
    …
  }
  ```

- The decide commit says the decisions came from a verdicts file, and who made them. `the decide commit (git log)`

  Now:

  ```text
  docs(specs): decide log-rotation q-keep, q-scope

  Decided from a verdicts file, transcribed in a headless session (claude-sonnet-5-5 / medium on claude-code).

  Artifact: docs/specs/2610201015-log-rotation.md
  Decided-By: operator (Ada Lovelace)
  Decided-Via: verdicts
  Decision-Id: 2610201015-log-rotation/01
  ```

  :::details Before

  ```text
  docs(specs): decide log-rotation q-keep

  Decided in an interactive session (claude-opus-5-5 / high).

  Artifact: docs/specs/2610201015-log-rotation.md
  Decided-By: operator (Ada Lovelace)
  Decision-Id: 2610201015-log-rotation/01
  ```

  :::

- The records say a decision was transcribed from a verdicts file, which questions it settled and how many departed from the recommendation. `decision record on phax/records/v1`

  Now:

  ```text
  decision/2610201015-log-rotation/01/record.json   (beside verdicts.json, prompt.md, document.json, body.diff)
  {
    "$schema": "https://docs.phax.run/schemas/decision-record-manifest/0.28.0.json",
    "kind": "decision",
    "decisionId": "2610201015-log-rotation/01",
    "mode": "verdicts",
    "questions": ["q-keep", "q-scope"],
    "arbitrated": 2,
    "departed": 1,
    "outcome": "committed",
    …
  }
  ```

  :::details Before

  ```text
  decision/2610201015-log-rotation/01/record.json
  {
    "$schema": "https://docs.phax.run/schemas/decision-record-manifest/0.25.0.json",
    "kind": "decision",
    "decisionId": "2610201015-log-rotation/01",
    "mode": "interactive",
    "questions": ["q-keep"],
    "arbitrated": 1,
    "departed": 0,
    "outcome": "committed",
    …
  }
  ```

  :::

## Also in this release \{#also-in-this-release}

### Release notes from specs \{#release-notes-from-specs}

At the release cut, phax's release notes are written from what the release's runs delivered: each completed spec with its guiding rule and the surfaces it changed, each plan without a spec, the commits made outside any run, and the release's upgrade steps. The cut writes them to `docs/releases/<X.Y.Z>.md` in the release commit, the same file becomes a page of the docs site, its short form (the file without its folded before/after blocks, plus a link to that page) becomes the GitHub release body, and `pnpm release:notes` previews them at any time without writing anything.

### Readable release notes \{#readable-release-notes}

The release notes read as an announcement. A Highlights section opens them, each spec's surfaces are bullets that start with their sentence, a surface's current example is labelled `Now:` with the earlier one folded under `Before`, and nothing is said about work that has not shipped. The builder in `scripts/releaseNotes.ts`, the GitHub release body and the docs site page follow the new layout.

## Fixes \{#fixes}

- fix(plans): give headless planning the spec's criterion ids; guard source-spec paths; review fixes for #164 ([507fc53](https://github.com/lbdremy/phax/commit/507fc536099ae0d010104398ec67d92406efe533))
- fix(decide): restore a recommendation after a departure in verdicts mode ([508dd1c](https://github.com/lbdremy/phax/commit/508dd1c316ab73c88a4cf1880367093eab9f26f5))
- fix(decide): refuse a Vibe writer, any non-operator --by and a terminal stdin ([0fc83a2](https://github.com/lbdremy/phax/commit/0fc83a2666c19b0e0e39e7c22a5e17abbf14caf8))
