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, and this page is available as Markdown at https://docs.phax.run/releases/0-28-0.md.

phax 0.28.0

Changes since 0.27.0: v0.27.0...v0.28.0.

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

  • 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

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:

    "testStrategy": [
      {
        "guarantee": "every month boundary rounds as the table says",
        "size": "small",
        "technique": "table",
        "oracle": "boundary table",
        "loop": "oracle",
        "criterion": "ac-month-boundary"
      }
    ]
    Before
    "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:

    {
      "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"
        }
      ]
    }
    Before
    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:

    ### 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
    Before
    ### 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:

    $ 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
    Before
    $ 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:

    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:

    $ 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:

    $ 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:

    $ 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:

    ## 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).
    Before
    ## 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

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:

    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
    Before
    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:

    {
      "$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:

    "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. …
    Before
    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:

    $ phax artifact schema verdicts
    {
      "$schema": "http://json-schema.org/draft-07/schema#",
      …
      "type": "object",
      "required": [
        "$schema",
        "artifact",
        "verdicts"
      ],
      …
      "title": "phax decide verdicts"
    }
    Before
    $ 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:

    $ 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:

    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
    Before
    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:

    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",
      …
    }
    Before
    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

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

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

  • fix(plans): give headless planning the spec's criterion ids; guard source-spec paths; review fixes for #164 (507fc53)
  • fix(decide): restore a recommendation after a departure in verdicts mode (508dd1c)
  • fix(decide): refuse a Vibe writer, any non-operator --by and a terminal stdin (0fc83a2)