Audit-grade workflow for AI-assisted software development.
PHARN turns an AI coding session into a persistent engineering record: the intent that anchored the change, the plan the agent followed, the files it declared, the checks that ran, and the handoff at the shipping gate.
It is open-source methodology, not a black box: Claude Code commands, readable Markdown artifacts, deterministic hooks, stdlib-only floor checkers, grillers, and review lenses that live in your repository. PHARN is strict about one thing: claims backed by deterministic checks are named as such; model or human judgment remains advisory.
npx @pharn-dev/pharn@latest initStatus: Ready to install and use with Claude Code today. Active development continues; functionality that has not shipped yet is explicitly labeled.
- What PHARN is
- Why it exists
- Quick start
- What gets installed
- How the workflow works
- Commands
- Capability coverage
- Why not just CLAUDE.md or AGENTS.md?
- Guaranteed vs advisory
- The pipeline
- PHARN builds PHARN
- Current limitations
- Design docs
- Contributing
- Security
- License
PHARN is an open-source workflow layer for teams using AI agents to change real code. It gives each increment a committed paper trail:
SPEC.md— the human-readable intent PHARN asks you to approve before implementation (or, under the unattended/pharn-loop, approves for you and records as the model's approval).PLAN.md— the agent's implementation plan and declared write scope.GRILL.md— pre-build interrogation of the plan (a quick run'sGRILL.md—/pharn-ship --quickor/pharn-loop --quick— records only its two floor stops and explicitly did not interrogate — see--quickunder Quick start).AC-TESTS.md,AC-TESTS.lock.json— which test covers each acceptance criterion, and the pinned evidence that each of those tests failed before the build.BUILD.md,REGRESSION.md,VERIFY.md,SHIP.md— what changed, what ran, what passed, what did not, and where the run stopped (a quick run writes noREGRESSION.md: it never runs/pharn-regress; an unattended loop writesLOOP.mdinstead ofSHIP.md, and a quick loop'sLOOP.mdrecordsmode: quick).cost.json,RUN-REPORT.md— what the run cost and what it did, written by/pharn-loopat every stop and by/pharn-shipat every exit that ends a run. The first is machine-readable token counts per stage, iteration and model; the second is the human-readable view over it. Tokens only — PHARN ships no price table, so money is your own list price applied to those counts. (A quick run keepscost.jsonbut writes noRUN-REPORT.md.)
The project is intentionally small-surface: prompts and contracts in Markdown, plus deterministic Node helpers. There is no hidden service in this repository and no proprietary rule engine needed to inspect what PHARN is doing.
PHARN is not a correctness oracle, a security guarantee, or a replacement for tests and code review. It preserves intent, narrows some agent write behavior, runs focused plan/code scrutiny, and labels the boundary between deterministic checks and judgment.
AI made code cheaper to produce. It did not make code cheaper to understand six weeks later.
The expensive questions moved upstream and downstream:
- What did we ask the agent to build?
- Which constraints shaped the plan?
- Which files was the agent supposed to edit?
- What did the workflow actually check?
- Which findings were deterministic, and which were model judgment?
- What should a reviewer, maintainer, or future incident responder trust?
PHARN puts those answers in the repo, where normal engineering tools can diff, review, and preserve them. The goal is not to make AI development look clean. The goal is to make it inspectable.
PHARN runs on Claude Code. The @pharn-dev/pharn installer requires
Node 20 or newer. The deterministic floor checkers this repo ships (pharn/floor/*.mjs, invoked by the
/pharn-* stages) require Node 24.2 or newer: their CLI entry points gate on import.meta.main, which
Node added in 22.18 / 24.2. On an older Node a guarded tool can exit 0 without running its checks — a
silent false green for several gates. CI and local contributor gates use Node 24. In your project root:
npx @pharn-dev/pharn@latest initThen open Claude Code in the same project and run the full loop:
/pharn-loop implement password reset with a one-time token
/pharn-loop runs spec → plan → grill → test → build → regress → verify unattended. The model approves its
own spec, writes each acceptance criterion's test and shows it fails before any code exists, repeats build →
regress → verify until a deterministic stop — green, the --max-iter cap, or a
result it must not retry — commits a green result to a new local branch (never pushed or merged), and
ends with a summary of what was done. When it reaches a point that needs a human decision, it stops and
says what it needs instead of guessing. On any stop other than a committed green result it reverts its
own spec approval — a procedural step, so an aborted run can skip it.
The test stage needs your project's test runner and its per-test results: a test script (and a test:e2e or
e2e script for end-to-end criteria) whose reporter writes JSON to the path PHARN passes
(Per-test results). Without them the run stops with blocked: no-test-runner and suggests a
setup increment to run first. The same split applies when a feature must change the runner itself: its config at
the project root, its test script, or its results format. PHARN pins those before the build, so plan that change as
its own setup increment first, run through /pharn-ship. /pharn-plan refuses a plan that names a root runner
config.
To approve the spec yourself and decide merge, fix, or abandon at the end, use the one-pass run with both human gates:
/pharn-ship implement password reset with a one-time token
For a small, well-scoped change, --quick (6.25.0) runs a shorter spine — both human gates, a
spec_kind: quick mini-SPEC of 1–3 criteria, the grill's two floor stops without its interrogation,
test-first evidence and /pharn-verify — and skips /pharn-regress's base-and-head comparison, the plan
interrogation, BRIEFING.md and RUN-REPORT.md. Its ship record lists what it did not check, and a changed
file outside the plan's declared files still stops the run (/pharn-regress's scope check is kept).
/pharn-ship --quick fix the off-by-one in the pagination cursor
--quick counts only as the first token of the arguments. That rule is an instruction to the orchestrating
model, not a parser, so it is advisory. The deterministic backstop is the SPEC: quick mode first reads the
approved, pinned spec_kind and stops on anything but quick, so a misread --quick over a full SPEC meets
that stop. It cannot tell a typed --quick from a misread one over a SPEC you already approved as quick.
The unattended loop has the same mode (6.28.0), without the human gate:
/pharn-loop --quick fix the off-by-one in the pagination cursor
The model writes and approves the quick SPEC, so nobody is told the trade before the run; an intent that does
not fit a quick SPEC stops the run (blocked: not-quick) instead of widening into a full one. Every iteration skips
/pharn-regress's base-and-head comparison and keeps its scope check, and the stop is decided over /pharn-verify's
verdict alone — in a table the SPEC's pinned kind chooses, never the flag. Its green is STOP_GREEN_QUICK, which is
not STOP_GREEN; the record (LOOP.md, mode: quick), the commit message and the summary name the mode and list
what was not checked. It writes cost.json but no RUN-REPORT.md.
Every stage is also available as its own command; see Commands.
Already installed? npx @pharn-dev/pharn status reports your installed skills version and drift.
update re-fetches the latest skills version, add and remove manage capabilities, and list prints
what is installed. init installs into the project rather than onto your PATH, so keep the npx
prefix unless you installed the CLI globally.
Two version numbers, on purpose. The pharn badge above tracks
SKILLS_VERSION — the content an install receives, and what status and
CHANGELOG.md are keyed to. The npm package @pharn-dev/pharn carries the installer's own version.
They move independently and are not meant to match.
The installer reads your project and selects the capabilities that apply before writing files. Detection
is JS/TS-shaped today: it reads package.json and scans for next.config.*, app/ route handlers,
.tsx/.jsx, migrations/, and .sql, resolving to one or more of ssr, backend, spa, and lib
— a project can match several rather than exactly one. (The installer's own documentation gives Next plus
Express as an example resolving to ssr and backend together.) A repo with none of those signals still
installs the universal capabilities.
You see the selected capability list, with a reason beside each entry, before the installer writes. A normal install adds:
- product commands in
.claude/commands/, - hooks in
.claude/hooks/: the two write guards, the scope setter they read, and aStopguard for/pharn-loop(see below), - the four trusted docs —
pharn/CONSTITUTION.mdandpharn/ARCHITECTURE.md, plusTHREAT-MODEL.mdandLIMITS.mdat the root, - the deterministic floor, contracts, grillers, and review lenses under
pharn/, pharn.config.json, pinning the skills version and exact installed commit, and carrying themodels.stagesblock that sets each product command's model and effort — and, since 6.27.0, the model/pharn-shipand/pharn-looprequest for each stage they run as a subagent (see Current limitations for what that does and does not reach).
your-repo/
├── .claude/
│ ├── commands/pharn-*.md # the 11 product commands
│ ├── hooks/*.cjs # the write guards, their setter, the /pharn-loop Stop guard
│ └── settings.json # wires the hooks (see the caveat below)
├── pharn/
│ ├── CONSTITUTION.md # trusted docs (human-only)
│ ├── ARCHITECTURE.md
│ ├── floor/*.mjs # the deterministic checkers
│ ├── pharn-contracts/ # artifact shapes
│ ├── pharn-pipeline/grillers/ # plan interrogators
│ ├── pharn-review/ # code lenses
│ └── features/<name>/ # per increment, written as you run the pipeline — commit these:
│ # SPEC PLAN GRILL AC-TESTS (+ .lock.json) BUILD REGRESSION VERIFY, then SHIP + BRIEFING
│ # (/pharn-ship) or LOOP (/pharn-loop), and RUN-REPORT + cost.json
├── THREAT-MODEL.md # the other two trusted docs
├── LIMITS.md
├── pharn.config.json # skills version + installed commit + models.stages
└── .pharn/ # runtime scratch — add to .gitignore
The hooks enforce only after they are registered in Claude Code's settings — .claude/settings.json, or
.claude/settings.local.json, which is loaded too and can wire or override the same hooks. If your
project already has a .claude/settings.json, the installer preserves it and warns instead of
overwriting it. Until you copy the hook wiring over, any guarantee that depends on a PreToolUse hook is
not active.
require-loop-record.cjs is not a write guard. It is a Stop hook: while an unattended /pharn-loop run
in the session has written no LOOP.md, it refuses to let the turn end, a bounded number of times per run,
and it fails open. It does nothing unless your settings register it under Stop. As of 6.12.0 the
settings.json PHARN ships registers it (matcher-less, exec form). An existing install whose
settings.json the installer preserved still needs that entry copied by hand — pharn update never
edits it.
Copy the wiring as it ships, anchored on the project-directory placeholder:
"command": "node \"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/protect-trusted-paths.cjs"Claude Code runs a hook in Claude's current directory, so a relative node .claude/hooks/… stops
starting after any cd into a subdirectory: node exits 1, which Claude Code treats as a non-blocking
error, and both guards are then silently off. That was the shipped form until 6.1.0, and it is
measured, not theorised. Upgrading an older install is ordered: run pharn update first, then change
these two commands — the anchored wiring over pre-6.1.0 hooks regresses both guards, so roll back in the
reverse order. Each guard judges the git working tree that contains Claude's current directory;
LIMITS.md §7 states the bounds that remain.
PHARN splits an AI-assisted change into typed stages:
- Spec — turn prose intent into
SPEC.md, surface gaps, and stop for approval. - Plan — turn the approved spec into
PLAN.md, including the concrete files the build may touch. - Grill — interrogate the plan before code exists.
- Test — write each acceptance criterion's test, run it, and require it to fail before any code exists.
- Build — implement the plan. With the hooks wired as they ship, Claude Code write/edit tools are denied outside the active scope, in whichever working tree Claude is currently in.
- Regress — re-run existing project suites and record breakage outside the feature.
- Verify — run the project's gates, check declared artifacts and completeness signals (including missing concrete paths), and check that each acceptance criterion's locked test now passes.
- Ship — write the ship/briefing artifacts and present the final human decision gate.
The approved spec body is pinned by a content hash, so later drift is detectable. Verification can also
report INCOMPLETE when a concrete path declared by the plan does not exist after the build.
Those are narrow checks, not magic. PHARN does not prove that the plan was wise, that every finding is correct, or that the final code is secure. It makes the workflow legible and backs specific claims with specific deterministic checks.
Two commands cover the normal path. Every other command except /pharn-review and /pharn-memory-promote is
a pipeline stage those two run, available on its own when you want to inspect or drive one step manually. Those
two are standalone: neither is a pipeline stage, and neither is invoked by /pharn-loop or /pharn-ship.
| Command | Use it when you want to... |
|---|---|
/pharn-loop |
Run the full workflow unattended: the model approves the spec, iterates build → regress → verify to a deterministic stop, commits a green result to a local branch, and reports. --quick (6.28.0): a quick SPEC, no base comparison. |
/pharn-ship |
Run the full workflow once, then present the ship record and briefing at the final human decision gate. --quick (6.25.0) runs a shorter spine for a small change and skips /pharn-regress's base comparison. |
/pharn-review |
Run code-review lenses in parallel over any code and merge their structured findings deterministically. This is standalone; it is not a pipeline stage. |
/pharn-spec |
Convert prose intent into a structured SPEC.md, surface gaps, and stop for approval before implementation. |
/pharn-plan |
Convert an approved SPEC.md into a PLAN.md with declared files and declared promoted lessons. |
/pharn-grill |
Challenge the plan before code exists and re-check the spec/plan hash chain. |
/pharn-test |
Write each acceptance criterion's test before the build, into the files /pharn-plan mapped in AC-TESTS.md, run them and require each to fail, and pin the evidence. Runs between /pharn-grill and /pharn-build in /pharn-ship and /pharn-loop. |
/pharn-build |
Implement the plan after setting the active write scope from PLAN.md. |
/pharn-regress |
Re-run existing project suites and record regressions outside the feature. |
/pharn-verify |
Check build artifacts and completeness signals, including declared concrete paths that were never created, and that every acceptance criterion's locked, once-red test passed. |
/pharn-memory-promote |
Promote one lesson into memory-bank/ through a gated provenance check. |
The command names are generated and drift-guarded in the inventory below; the one-line descriptions in this table are hand-written and are not.
Capabilities are named for the problem they inspect, not the framework they run in. Grillers interrogate a plan before implementation; lenses read code after implementation or during standalone review.
Grillers — a11y, architecture, comprehension, coupling, documentation, error-handling, i18n, migrations, observability, performance, privacy, security, testability.
Lenses — injection, SSRF, path traversal, insecure crypto, unsafe deserialization, secrets in code,
input validation, hallucinated APIs, missing await, missing timeouts, null dereference, off-by-one,
race conditions, resource leaks, swallowed exceptions, missing error handling, n-plus-one queries,
duplicated logic, copy-paste drift, magic values, placeholder-as-done, and a trust fence.
Every capability ships with eval cases and expected outputs. The floor refuses a capability whose declared rules are not exercised by at least one eval.
This section is a tour, not the authoritative inventory. The drift-guarded lists are generated from the
repository: docs/capabilities/ and the
current-state block below.
Keep them. PHARN is not a replacement for a project instructions file.
CLAUDE.md, AGENTS.md, and similar files are instructions the model may follow. PHARN adds versioned
workflow artifacts plus deterministic checks that do not depend on the model deciding that a sentence
should be obeyed.
flowchart LR
A["agent proposes a write"] --> H{"PreToolUse hook"}
H -- "path is in the plan's declared scope" --> OK["write proceeds"]
H -- "trusted doc, or outside that scope" --> D["exit 2 — denied"]
BASH["the same write, issued via Bash"] -. "matcher excludes Bash —<br/>neither hook runs" .-> UN["write proceeds,<br/>unblocked"]
UN -. "re-hashed at /pharn-verify" .-> REC{"would the guards<br/>have denied it?"}
REC -- "yes" --> RED["reconcile gate fails<br/>— detected, not prevented"]
REC -- "no" --> OK
- A file states a rule. A hook can enforce one. PHARN's
PreToolUsehooks can deny writes through Claude Code's standard write/edit tools. - Approval becomes an explicit workflow gate. A spec remains
Draftuntil the user explicitly approves it. Downstream stages refuse a Draft or drifted spec. - Approved intent is pinned. A content hash makes later edits to the approved spec body detectable.
- Findings cite stable rule IDs. A review finding names the rule it violates instead of relying on remembered chat context.
- Guaranteed decisions avoid free-text judgment. Deterministic gates operate on paths, hashes, enums, regex matches, exit codes, and other bounded values rather than asking the model whether something "looks safe."
PHARN still relies on agent orchestration to invoke parts of the workflow. It does not turn an LLM into a trusted execution environment.
This distinction is the core design rule.
A guarantee must reduce to a deterministic, non-LLM operation such as a hook decision, content-hash comparison, enum/set-membership check, regex scan, or filesystem check. Anything that depends on model judgment is advisory.
Guaranteed — examples of narrow claims backed by named checkers:
| Guarantee | The check behind it |
|---|---|
The four trusted docs — and the guards' own control surface, and your project's own SPEC template (pharn.spec-template.md) — cannot be edited through Claude Code's Write/Edit/MultiEdit/NotebookEdit surface |
.claude/hooks/protect-trusted-paths.cjs |
Memory-bank canon (memory-bank/, .dev/memory-bank/, subtrees included) is denied on that same surface, unless the active writes-scope was set by a promotion command and names that one canon file alone — so a build plan cannot grant itself a canon write |
.claude/hooks/protect-trusted-paths.cjs (origin read from set-writes-scope.cjs's argv) |
Writes through that same tool surface — and only that surface, since the wired PreToolUse matcher does not match Bash — are restricted to the active write scope; with none active the default is fail-closed in a dev checkout or an unsignalled tree, and — since 6.24.0 — in an installed project too, but only while a /pharn-ship, /pharn-loop or /pharn-review run is open; outside a run an installed project's default instead denies PHARN's own reserved surface and its scope file and allows the rest of the project, and outside the project allows only Claude Code's memory folders and the temp roots |
set-writes-scope.cjs + enforce-writes-scope.cjs + run-marker.mjs |
The verify and regress verdicts are computed from a gate map the gate runner wrote, not one a model typed: each value is the exit code the runner recorded for the listed command, the keys cover the resolved gate set (plus reconcile for verify, which runs last), and no tree edit happened between consecutive gates |
run-gates.mjs, validated by check-verify.mjs --stamp and check-regress.mjs verdict --base-stamp … --head-stamp … |
A non-adversarial write that reached a path the active scope would have denied — including one issued through Bash, which no hook sees — is detected between build and verify, and fails the verify verdict. Detected, not prevented; git-ignored paths are outside the reconciled set; and a writer who also rewrites the baseline defeats it on ordinary paths |
reconcile-baseline.mjs --anchor + check-bash-reconcile.mjs, feeding check-verify.mjs |
| An approved spec is pinned, so later body drift is detectable | check-spec.mjs --hash at approval; re-verified at plan, grill, test, build, regress, verify and ship by check-spec-approved.mjs (directly at plan, test and ship; through check-plan-spec-agree.mjs at grill, test, build, regress and verify) |
A SPEC that declares spec_template has the template's sections, acceptance criteria each with an id, Given → When → Then and exactly one verify: level, and no clarification marker once approved. That the criteria are phrased testably — not that any test exists, runs, or passes; and opt-in, so a SPEC without the key is checked as before |
check-spec.mjs (the rules in pharn/pharn-contracts/spec-template.md) |
A template — the shipped default or your own — is refused before /pharn-spec can pin it unless it has the required sections, a visible example criterion, the out-of-scope label and a spec_template: line; an existing but invalid project template stops the run instead of falling back to the default. A minimum shape — not that a SPEC filled from it will pass |
check-spec.mjs --resolve-template-ref (the refusals in pharn/pharn-contracts/spec-template.md) |
| Secret-shaped literals in a plan can be detected by the shipped regex scanner | scan-plan-secrets.mjs |
| A missing concrete path declared by the plan yields an incomplete build signal | check-build-complete.mjs feeding check-verify.mjs |
/pharn-build does not start until every acceptance criterion of a templated SPEC has a test, in the file the mapping names, that was collected and failed in a run bound to the pinned test files and the live tree (a spec_kind: test-infra SPEC gets a weaker bootstrap lock, labelled as such). That the record says failed — not why: a test failing on its own typo reads the same |
check-test-stage.mjs, over check-ac-tests.mjs, check-red-run.mjs and ac-tests-lock.mjs |
/pharn-verify fails unless each acceptance criterion of a templated SPEC has a locked, once-red test titled AC-<n>:, in a file mapped to it, that passed on the head run — and fails too when those tests, the lock or the pinned test infrastructure changed after /pharn-test. That the reporter said passed — not that the test captures the criterion's intent, and the infrastructure pin covers a closed set of files |
check-verify.mjs --ac-gate (ac-gate-core.mjs) |
| Which lenses run, and how structured findings merge | count-lenses.mjs + merge-findings.mjs |
The eleven product commands' model: / effort: frontmatter equals what pharn.config.json's models.stages resolves for that stage — not that the stage ran under it |
check-model-config.mjs |
A run's cost.json is internally consistent: a closed top-level key set, every aggregate equal to a recompute from the recorded requests, unique request ids, a strictly increasing marker sequence, no absolute path anywhere, and every row inside the run window recomputed from its own recorded markers and, since 6.29.0, from a context in its own recorded context set. Consistency only — not that the numbers describe the run, nor that the recorded markers and contexts are the run's |
check-cost-ledger.mjs |
Advisory — everything a model judges: whether a plan is wise, whether a review finding is real, whether a severity is right, whether the code satisfies the product intent, and whether the resulting system is well designed. These findings are surfaced for a human; they are not converted into guarantees by wording them strongly.
Important bounds:
- The write guards cover Claude Code's Write/Edit/MultiEdit/NotebookEdit tool surface. Writes performed through Bash bypass those hooks.
scan-plan-secretsdetects configured patterns; it does not prove that a matched literal is a live secret or that an unmatched plan contains none.check-build-completeproves that declared concrete paths exist. It does not prove that the build modified them, or that their contents are correct.check-cost-ledgercertifies that acost.jsonagrees with itself. It does not bind the recorded requests to the session that produced them, so a self-consistent fabricated ledger passes — a test in the repository proves it by building one. The--verify-transcriptflag re-derives the rows from the live transcript, but a transcript is machine-local and Claude Code prunes it on its own schedule, so that check is deliberately not a gate. The ledger annotates a run; it gates nothing.- A validating gate-run stamp proves internal consistency, not provenance. A self-consistent fabricated
stamp passes, and a test in the repository builds one to prove it. The stamp alone also says neither
that the stage ran nor that the report on disk is its output. Under
/pharn-loop,check-loop-fresh.mjsnarrows that by binding each report to its stamp by hash and the verify stamp to the live tree. That is tree identity, not recency. check-model-configcompares two files. Model and effort are applied by the Claude Code platform, so nothing here observes that a stage ran under the configured model — and an orgavailableModelsallowlist or auto mode can decline a value silently.- A green PHARN floor means the named deterministic checks passed. It does not mean the code is correct.
See LIMITS.md for the full set of bounds.
Eight typed stages, each emitting a typed artifact:
flowchart LR
S["spec"] --> G1{{"SPEC approved<br/>(by the model under /pharn-loop)"}}
G1 --> P["plan"] --> GR["grill"] --> T["test"] --> B["build"] --> R["regress"] --> V["verify"]
V -- "measurable red, under the cap<br/>(/pharn-loop only)" --> B
V -- "green, cap reached,<br/>or a red it must not retry" --> G2{{"human decides<br/>merge / fix / abandon<br/>(/pharn-loop: after its summary)"}}
G2 --> SH["ship"]
What binds the chain is the SPEC→PLAN content-hash, not a field on every artifact and not a
stage-to-stage handoff. Identity travels as the feature slug — spec_id ≡ <name> ≡ the feature
directory — and check-plan-spec-agree.mjs re-verifies the pin at five downstream stages (grill,
test, build, regress, verify). A literal spec_id field appears in SPEC.md (the root identity, read by
check-spec.mjs --spec-id), PLAN.md and BRIEFING.md — not on every artifact.
Stages do not each read the previous one's output. /pharn-regress reads the plan to derive the
inside/outside scope boundary; /pharn-verify reads the plan, its own gates and, for the acceptance-criteria
check, the SPEC's criteria, AC-TESTS.md and the lock, and it does not read regression-report.json at all. The stage that reads everything is /pharn-ship, which orchestrates
the chain and preserves two human decision points: explicit spec approval before planning, and the
final merge/fix/abandon decision after verification.
The orchestration itself is not a deterministic guarantee: the agent invokes the stages. The proceed/stop decisions inside the pipeline are read from the deterministic verdicts emitted by the relevant checkers.
/pharn-loop runs the same chain without either human gate. The model approves the spec, and the build →
regress → verify middle repeats until a deterministic stop: green, the iteration cap, or a red it must not
retry (an inconclusive result; a reconcile red — a retry would re-anchor the baseline and erase the
detected escape; or, since 6.20.0, acceptance-criterion evidence that changed after /pharn-test, which another
build cannot restore — blocked: ac-evidence-invalid). Before it reads that stop, and again before it commits, check-loop-fresh.mjs checks that
the evidence belongs to the tree: each report must be its checker's output from a stamp that validates, bound
to it by hash, and the verify stamp must describe the live tree. A skipped or stale stage is re-run inside
the same iteration under a counted budget. A forged verdict or a spent budget ends the run as a recorded
blocked stop, not as a summary that names the skipped gates. That is tree identity, not recency: an iteration
whose build changed nothing can still reuse the previous iteration's evidence. Only a green result is
committed, to a new local branch, and only if its recorded decision re-derives from the reports it cites
(check-loop-decision.mjs re-runs the loop's stop computation and compares); a green that does not
re-derive is not committed. None of this proves the reports are honest: a self-consistent forged set of
stamps and reports still passes. Every other outcome reverts a
model-approved spec to Draft, or the run says it could not; a spec the run never approved (a stop on a
clarification marker, say) simply stays a Draft. The human decision comes after the run, on the branch or the
working tree it leaves.
Standalone: /pharn-review is not a pipeline stage. It runs review lenses in parallel as subagents
and merges their structured findings deterministically. You can run it against code independently of the
shipping pipeline.
/pharn-spec fills PHARN's default SPEC template unless your project has its own. To use your own, copy
pharn/pharn-contracts/templates/spec-template.md to pharn.spec-template.md at the project root and edit it
there: rename or reword sections, change the guidance comments, adjust the example. /pharn-spec then picks it
up on its next run, and every SPEC it writes records project@sha256:… as its template.
Edit that file yourself, outside Claude Code's write tools. This is by design: its guidance comments are
instructions /pharn-spec follows, so the path is fixed and the write guard denies Write/Edit to it, whether or
not the file exists. An existing install gets that protection when pharn update replaces the hook script. A
Bash write is not blocked (see Current limitations), and neither is a change that
arrives by pull request or git pull: review edits to this file like code.
Before /pharn-spec fills a template it is checked. It needs the five required sections (Intent, Scope,
Acceptance Criteria, Constraints, Assumptions) as visible headings, one example criterion in the - **AC-<n>** Given … When … Then … shape with one verify: line, the **Out of scope…** label under Scope, and a
spec_template: line in its frontmatter. It must also be a regular file named exactly pharn.spec-template.md,
not a symlink, and PHARN's own pharn/ must not be reached through a symlink. If your template fails, /pharn-spec stops and names the reason; it does not fall back to the
default. Deleting the file later switches future SPECs back to the default, and it does not affect SPECs
already approved.
A gate's exit code says whether the whole suite passed, not whether one named test ran: a suite exits 0 with
a skipped test. PHARN can also read a per-test record — each test's id, file, title and passed, failed or
skipped — from a JSON report your test runner writes. /pharn-test reads it, and since 6.20.0 so does
/pharn-verify's acceptance-criteria check (both below).
To turn it on, name your reporter's format for each gate in pharn.config.json. The gates are test and the
e2e gates (test:e2e, e2e). The formats:
vitest-json,jest-jsonandplaywright-jsonare each their runner's own built-in report, so there is nothing to install.pharn-jsonis a neutral format PHARN defines. Use it for any other runner.
For example:
{ "testResults": { "test": "vitest-json", "test:e2e": "playwright-json" } }An e2e gate exists only when your package.json has a test:e2e or e2e script. /pharn-verify runs it after
build, last among your project's gates; /pharn-regress never discovers it (a gate you name yourself with
--gates still runs). It gets the same per-gate time limit as every other gate (540 s), and
under /pharn-loop it runs on every iteration. Starting servers and installing browsers stay your script's job.
Then have the reporter write to the path PHARN passes in PHARN_TEST_RESULTS. PHARN sets that variable only
while its own stages run your gates, so an ordinary test run is unchanged. With vitest:
// vitest.config.js
import { defineConfig } from "vitest/config";
const results = process.env.PHARN_TEST_RESULTS;
export default defineConfig({
test: {
// ...your other test options...
...(results ? { reporters: ["default", "json"], outputFile: { json: results } } : {}),
},
});With Playwright:
// playwright.config.js
const { defineConfig } = require("@playwright/test");
const results = process.env.PHARN_TEST_RESULTS;
module.exports = defineConfig({
// ...your other options...
...(results ? { reporter: [["list"], ["json", { outputFile: results }]] } : {}),
});With Jest ("test": "jest-json"), which has no config option for its JSON report, add the flags in the script.
The ${…:+…} form adds them only while the variable is set:
{
"scripts": {
"test": "jest ${PHARN_TEST_RESULTS:+--json \"--outputFile=$PHARN_TEST_RESULTS\"}"
}
}That form needs a POSIX shell. On Windows npm runs scripts under cmd.exe, which hands the text to Jest
unexpanded. Things to know about Jest:
- Name
jest-json, notvitest-json.vitest-jsonalso parses Jest's report, because vitest copies its shape. But it skips Jest's retry andtest.failingfields, so under it those tests read as passes. - The record was checked on Jest 29.7.0 and 30.5.2. A report from a Jest that does not write
invocationsis refused rather than guessed. - Jest reads the file arguments PHARN passes as path patterns, so a similarly named test file can run too, and its
tests enter the record. Jest's
--runTestsByPathmakes them exact paths. - The lock's test-infrastructure pin covers a
jest.config.*file and, since 6.31.0, ajestkey insidepackage.jsontoo.
For any other runner, write a small reporter that emits pharn-json. The schema, with an example, is in
pharn/pharn-contracts/test-results-record.md, "The neutral format". Name the reporter file literally in the test
script (--reporter=./tools/pharn-reporter.mjs, say): since 6.31.0 the lock pins every file a test script names
that way, and a plan that puts it in the build's scope is refused.
What the record can and cannot tell you is in pharn/pharn-contracts/test-results-record.md. In short, "passed"
means your reporter said so. A flaky test or an expected failure that the report marks is never counted as a pass:
Playwright's flaky and test.fail(), Jest's pass on a retry, and Jest 30's test.failing. Since 6.31.0 the record
lists such a test, and every test name two tests share, as an anomaly beside the tests it can read. An anomaly
decides a criterion only when it sits in a file that criterion maps, and is reported otherwise — before, one anywhere
in the suite voided the whole record. One the report does not mark reads as a pass. Measured cases are vitest's
test.fails and pass on a retry, and Jest 29's test.failing. pharn.config.json is not write-protected, so review
changes to it like changes to your test script.
For a SPEC filled from the template, /pharn-plan maps each acceptance criterion to a test file and the public
interface it drives (AC-TESTS.md), and /pharn-test writes those tests before /pharn-build, into files the
build is not allowed to write. Since 6.18.0 it also runs them and requires every criterion's test to fail:
a test that cannot fail, is never collected, or is skipped would otherwise pass unnoticed. The evidence — which
tests failed, bound to the files it pinned — goes into the committed AC-TESTS.lock.json. Since 6.19.0
/pharn-ship and /pharn-loop run it between /pharn-grill and /pharn-build, and /pharn-build refuses to start
until check-test-stage.mjs reads that evidence as complete. A feature planned or tested before 6.19.0 has to go
back through /pharn-plan and /pharn-test before it builds.
What it needs from your project:
- A runner for each criterion's level, with per-test results.
unitandintegrationcriteria run under yourtestscript,e2eones undertest:e2eore2e, and each of those gates needs its reporter configured as in Per-test results. Without them/pharn-teststops before writing anything and says which criterion has no runner. Set the runner up first, as its own increment: a SPEC withspec_kind: test-infrain its frontmatter gets a bootstrap lock instead — no tests before the build, which is weaker, and the lock says so. - Tests that import their target inside the test body. Before the build the module under test does not exist.
await import("../src/reset.js")inside the test makes that a failed test, which is what the red run wants; a top-levelimportmakes the whole file fail to load, so none of its tests is collected, and the red run refuses that as the wrong reason./pharn-testwrites them this way. The form must be one your runner can run, or the test fails before and after the build alike. Plain Jest in its default CommonJS mode cannot run an in-bodyawait import(), so there/pharn-testusesrequire()inside the test; Jest's ESM mode is the reverse. A runner that type-checks each file as it loads it (ts-jest with diagnostics on, for example) fails the file anyway; use its transpile-only mode. - An e2e runner that serves the app itself. The red run does not run
build; Playwright'swebServeroption is the usual way.
A criterion's test that already passes before the build is refused, with no override: either the test is
vacuous, or the behaviour exists and the criterion restates it. What the red run proves is limited to what the
record shows. "Failed" is the runner's status, so a test that fails on a typo of its own reads the same as one
that fails because the feature is missing. Details: pharn/pharn-contracts/ac-tests.md.
Since 6.20.0 /pharn-verify checks each criterion, not only whole gates. An AC is delivered = a locked, once-red
test titled AC-n:, in a file mapped to AC-n, passed on the head run. PHARN does not judge whether that test fully
captures the AC's intent. The match is by file, so another feature's AC-1: in the same suite never counts. The
verify report carries a per-AC table (id, level, matched tests, status, reason), and RUN-REPORT.md shows it.
- An AC not delivered yet (its test is missing from the run, failed, or skipped) fails verify, and
/pharn-loopbuilds again, like any failing gate. - AC evidence that changed fails verify and stops
/pharn-loop(blocked: ac-evidence-invalid): a pinned test or the lock was edited, the tests were never shown red, or the test infrastructure moved. Another build cannot fix that./pharn-testnow also pins what runs the tests: the level gates'package.jsonscripts (with theirpre/postscripts), theirtestResultsformat, and rootvitest/vite/playwright/jestconfig files — and, since 6.31.0, the scripts those chain to (npm run test:unit), the files they name (your reporter, a runner script), ajestkey inpackage.json, and a root.npmrc/.yarnrc/.yarnrc.yml. A plan may not put the named files, those configs or the feature's own lock in the build's scope; it may still namepackage.json(a dependency is ordinary build work), and a change there to anything the lock pins fails verify. What the pin does not see — code the build writes can still switch off assertions or the reporter from inside the test process, and a setup file a config imports, environment-driven configuration, a chain through another runner,tsconfigand the runner's version are unpinned too — is listed inpharn/pharn-contracts/ac-tests.md. After the build, re-running/pharn-testmeans setting the build aside first, because its red run would now pass. - What the wider pin costs (6.31.0). A source file a test script names literally is pinned too — a bundler entry
reached through
"pretest": "npm run build", a runner script — so a feature that edits it fails verify withtest-infra-changed; name a directory, a glob or a config file there instead, as its ownspec_kind: test-infraincrement. The lock records a digest of.npmrc: keep registry credentials in an environment variable or your user-level npmrc, never in the project's. - A feature locked before 6.20.0 has no infrastructure pin, and verify reports
test-infra-unpinneduntil it goes back through/pharn-testthat way. A feature locked by 6.20–6.29 keeps its lock; verify reportstest-infra-unpinnedonly if its test scripts chain to another script, name a file, or read ajestkey or a package-manager config — the things only the 6.31.0 lock pins — andtest-infra-changedif the 6.31.0 pin cannot be taken over its tree at all (a symlinked.npmrcor named file, a chain deeper than 8 scripts), which no 6.31.0 lock could record either. - A per-test record that cannot be read makes verify inconclusive, never a pass. A flaky test, an expected failure the report marks, or a duplicate test name makes a criterion inconclusive only when it sits in that criterion's test file; anywhere else it is listed in the report and decides nothing (6.31.0 — see Per-test results).
- A SPEC not filled from the template is reported
not-applicable (legacy spec)in the report, not silently passed. Aspec_kind: test-infraSPEC gets bootstrap evidence: the level's gate ran and reported at least one passed test. That is weaker, and the report says so.
PHARN is built with its own workflow, one increment at a time, and the resulting development artifacts are committed in this repository.
That means the repository contains real specs, plans, grill reports, reviews, regression reports, and verification records produced while building PHARN itself. You can inspect the process instead of taking the README's claims on trust.
The inventory below is generated from the live repository by npm run docs:generate and guarded
byte-for-byte by npm run docs:check, so it cannot quietly drift from what is actually built.
- Capabilities — 36 built, counted by the
role:frontmatter test (mirrorspharn/floor/validate.mjs): 13 grillers, 22 lenses, 1 skill (pharn/pharn-core/seam-resolver/), 0 validators, 0 verifiers, 0 auditors. Full list:docs/capabilities/README.md. - Contracts — 15 (
pharn/pharn-contracts/):ac-tests,cost-ledger,eval-format,finding-shape,gate-run-record,loop-record,reconciliation-record,regression-report,seam-config,ship-briefing,ship-record,spec-template,stage-exit,test-results-record,verify-report. - Product commands — 11 (
.claude/commands/):/pharn-build,/pharn-grill,/pharn-loop,/pharn-memory-promote,/pharn-plan,/pharn-regress,/pharn-review,/pharn-ship,/pharn-spec,/pharn-test,/pharn-verify. - Dev-apparatus commands — 9 (
.claude/commands/):/pharn-dev-build,/pharn-dev-eval,/pharn-dev-grill,/pharn-dev-memory-promote,/pharn-dev-plan,/pharn-dev-regress,/pharn-dev-review,/pharn-dev-ship,/pharn-dev-verify. - Hook scripts — 4 (
.claude/hooks/):enforce-writes-scope.cjs,protect-trusted-paths.cjs,require-loop-record.cjs,set-writes-scope.cjs. - Floor checkers — 101
.mjsfiles underpharn/floor/(tests excluded).
For concrete examples, browse .dev/features/. The development history includes
cases where PHARN's own review workflow raised defects in PHARN changes before those increments were
finished — see .dev/features/span-redos-linear/REVIEW.md,
where the review caught a false bound shipped by the very increment that was repairing a false bound.
PHARN is deliberately narrower than the claims many AI-development tools make.
- Claude Code only today. The current shipped integration uses Claude Code commands and hooks.
- Shell writes are still not PREVENTED — they are now DETECTED, and that is a weaker thing. The
PreToolUsematcher wired in.claude/settings.jsonisWrite|Edit|MultiEdit|NotebookEdit, and both hooks re-test that set in their own code, so a write issued throughBashnever reaches either one and is not blocked. Every write-guard guarantee on this page — the trusted-doc denylist, the canon denylist, and the writes-scope restriction — is scoped to that tool surface and to no other. What changed is that such a write no longer goes unrecorded:/pharn-buildanchors a content-hash baseline, and/pharn-verifyrunscheck-bash-reconcile.mjs, which re-hashes the tree and asks the live guards whether each changed path would have been denied. Denied ⇒ thereconcilegate fails and the verify verdict isFAIL. The claim is exactly "a write to a path the active scope would have denied is detected and fails the stage" — never "Bash writes are prevented." Four bounds, all inreconciliation-record.md: git-ignored paths are outside the reconciled set; the window is anchor→verify; the model is one worktree per session; and there is no attribution — it reports what, never who. A clean verdict means no escape was detected, not that none occurred. And this is accounting, not security. The baseline is unauthenticated state inside the writable tree, so a writer who edits a denied file and rewrites that file's baseline entry gets a silent clean result. What it reliably catches is a non-adversarial escape — a formatter, a generator, a script, a mistake, which is the whole population of failures it was built for. Deleting its state is loud (a missing baseline isINCONCLUSIVEat verify) and the always-reconciled control surface (.claude/hooks/*,.claude/settings*.json,pharn/floor/*,.dev/floor/*) is anchored in committed blob ids rather than the baseline, so that half resists a determined writer — but forging an ordinary path's entry does not. Two further bounds, stated rather than solved: the checker runs from the worktree, so it cannot vouch for its own integrity; and the anchor is a shell step, so a run that skips it silently reuses an earlier epoch instead of failing. The only true prevention remains OS-level sandboxing of theBashprocess, which PHARN does not implement — a harness-layer capability, not something markdown methodology can express, and the same category the missing authenticated baseline store falls into. - No shell command is ever parsed, and that is deliberate. The reconciler compares hashes and paths;
it never reads a
Bashcommand string. Shell parsing is undecidable and a verb denylist would be a heuristic, which the constitution forbids labelling a guarantee — sosed -i, a here-doc,node -e, a Makefile target and a compiled binary are all equally visible to it, and none is special-cased. - The write-scope guard's fail-closed default does not cover your source — except now, in an installed
project, only while PHARN is actually working (6.24.0). Where
enforce-writes-scope.cjsis wired and no scope is active, the posture depends on the tree. In PHARN's own dev repo (.dev/floor/present AND noskillsVersioninpharn.config.json) or an unsignalled tree, Claude Code's Write/Edit/MultiEdit/NotebookEdit tools may write onlypharn/features/**(plus, in the dev repo,.dev/features/**andpharn/pharn-*/**) — exactly as before. In an installed project (pharn.config.jsoncarries a non-emptyskillsVersion), that same narrow default now applies only while a/pharn-ship,/pharn-loopor/pharn-reviewrun is open — a marker file under.pharn/<command>/<name>/active.json, fresh within 24 h. Outside an open run, the guard instead denies PHARN's own installed surface —pharn/**exceptpharn/features/**,.claude/**andpharn.config.json, matched case-folded — plus its own input.pharn/writes-scope.json, and allows every other path inside the project, including your ordinary source (src/app.ts,package.json,README.md). That includes files Claude Code loads at session start — the project'sCLAUDE.md,AGENTS.md,.mcp.json— so a write made outside a run can shape later runs. Outside a run a path containing a backslash is denied too, because on macOS and Linux a backslash is part of a file name and the guards' path folding could otherwise judge a different file than the one written..pharn/**stays writable in every posture, as before — it is the gitignored runtime-state directory the guard bootstraps from, composed into the allow-list unconditionally, so it stays writable even under a set scope; one path inside it is still denied by name:.pharn/writes-scope.json. Outside the project the permissive default allows only two places — Claude Code's own memory folders (<claude-config-dir>/projects/*/memory/**, where the config dir is$CLAUDE_CONFIG_DIRor~/.claude) and the temp roots (the OS temp directory and/tmp) — and never a path inside another git tree, nor another spelling of the project's own path: a different letter case or Unicode form reaches the project's own files on a case-insensitive volume, so such a path is denied as the project's own. This is new in 6.24.0: before it, every out-of-project path was denied, as it still is in every other posture. Every other out-of-project path stays denied, including your dotfiles,~/.ssh,~/.claude/settings.json,~/.claude.jsonand~/.claude/hooks/.protect-trusted-paths.cjsis unchanged and still denies its own set (the trusted docs,CODEOWNERS, the guards' own control surface, your SPEC template) in every posture, regardless of any scope. A malformed.pharn/writes-scope.jsonnow denies every write in an installed project, rather than falling back to a default. The markers are written and removed through Bash:/pharn-ship,/pharn-reviewand/pharn-loopstop when opening theirs fails (the loop also when its pre-run snapshot does), but a run that skips the step leaves the install unguarded between stages, anything other than a directory at a run-state path (.pharn/pharn-review, say) holds the narrow default until someone removes it, and a crashed run's leftover marker holds the narrow default for up to 24 h unless it is closed or ages out. Clearing the scope (set-writes-scope.cjs --clear, or deleting.pharn/writes-scope.json) returns to whichever default the tree and the open-run state compute; it does not by itself re-open your source in the dev/unsignalled postures, or during an open install run. To write elsewhere, either set a scope that names those paths (set-writes-scope.cjs --from-plan <PLAN.md>) — noting that a set scope replaces the default rather than adding to it — or leaveenforce-writes-scope.cjsout of.claude/settings.json, at the cost ofwrites:enforcement. - The memory-bank denylist covers the write-tool surface only — Bash still reaches canon.
THREAT-MODEL.mdtreats memory-bank poisoning as the worst persistence vector. As of3.1.1.claude/hooks/protect-trusted-paths.cjsdoes denyWrite/Edit/MultiEdit/NotebookEdittomemory-bank/**and.dev/memory-bank/**, and the escape is the writes-scope record's origin (set_by), whichset-writes-scope.cjswrites from its own argv — so nowrites:declaration and noPLAN.md## Filesentry can grant itself a canon write. That closes the reported vector: a## Filesentry namingmemory-bank/lessons-learned.mdis now denied rather than silently allowed. What it does NOT close:PreToolUsehooks never see Bash, so an agent holding Bash can still append to canon, run the setter with promote-shaped argv, or forge the scope record. No mechanism without that hole was found and none is claimed. So "canon cannot be written" stays struck — what changed is that on the guarded tool surface a canon write now costs a separate, explicit, auditable act that a build plan cannot cause. Keep reviewingmemory-bank/**in diffs like any other file. - Model judgment remains model judgment. Architecture quality, review correctness, severity, completeness of intent, and semantic correctness are advisory unless a specific deterministic checker covers the claim.
- Approval is workflow discipline, not proof of a person. A content hash detects drift after a spec is
marked approved; it does not prove who marked it approved. Under
/pharn-loopthere is no person at all: the model approves and recordsapproved_by: model, a marker nothing gates on. - Build completeness is filesystem-level. PHARN can detect that a concrete declared path is missing; it cannot prove that an existing path was actually modified or implemented correctly.
- Prompt injection is not solved. PHARN narrows which data may influence guaranteed decisions, but it
does not claim to eliminate prompt injection. That includes skills you install yourself: a
.claude/skills/<name>/SKILL.mdreaches product stages as untrusted context, and a hostile one can talk a lens out of reporting a real finding, which then never reaches you. That is only partly bounded;THREAT-MODEL.md§2 (item 8) and §5 state it in full. - No verifier/auditor capability ships yet.
/pharn-verifyuses the shipped floor and project gates; the verifier plug-in slot remains empty. - Several announced modules are not built.
pharn-audits,pharn-skills-*,pharn-stack-*, and the rest ofpharn-core— the constitution engine, the agnostic rule set, and the memory-bank commands beyond promotion. What exists is what the generated inventory above lists. - Per-stage models are routed for most orchestrated stages, effort is not, and nothing proves what a
stage ran on.
pharn.config.json'smodels.stagesis the source of truth for the eleven product commands'model:/effort:frontmatter, andpharn/floor/check-model-config.mjsRED-fails when the two disagree — so editing the config is only half the change: update the command frontmatter too, or the checker will tell you. Since 6.27.0/pharn-shipand/pharn-loopalso read the block at run time: each stage their routing policy routes runs as a subagent, requested on the model the block resolves for it./pharn-ship's spec stage, every/pharn-regressand/pharn-verify, and any stage that falls back (no config; a pre-0.7.0 block, whichpharn updatewith@pharn-dev/pharn≥ 0.7.0 migrates;inherit; a full model id) run on the session's model, the model of the session running the orchestrator, and the run says why. Effort is not routed. A green checker means two files agree, and a route records what was REQUESTED:cost.jsonshows the model each request was served, which is evidence, never proof. The full bounds — turn scope, the platform veto, and what deleting the block costs — are stated once, inLIMITS.md§ 8. - It is token-hungry by construction.
/pharn-grillruns the grillers over your plan and/pharn-reviewfans every applicable lens out as its own parallel subagent;/pharn-looprepeats build → regress → verify up to the cap, unattended — each pass re-runs your suite at the base and at HEAD plus every verify gate. That buys parallel scrutiny and costs tokens accordingly. Budget for it, or drive individual stages instead of the loop. Since6.5.0a run no longer leaves you guessing what it spent:/pharn-loopand/pharn-shipwritecost.jsonandRUN-REPORT.mdinto the feature directory, with tokens broken down per stage, iteration and model. That is a measurement, not a reduction — it tells you the bill, it does not make the run cheaper. For an actual reduction on a small, well-scoped change,/pharn-ship --quick(6.25.0) is the manual lever: it drops/pharn-regress's base-and-head suite run (its scope check still runs), the plan interrogation, and the briefing/run-report renders./pharn-loop --quick(6.28.0) is the unattended one: it drops the same base-and-head run on every iteration, where the loop multiplied it, plus the interrogation and the run report. There is no automatic proportionality — nothing measures whether a change is "small" — the flag is a person's choice (under/pharn-loop --quickthe model then writes and approves the quick SPEC), and a change too large for it stops (S6c, under/pharn-loop --quick) or takes the full pipeline. - Packaging is still pre-release shaped. There are no GitHub releases or git tags yet; the installer
currently fetches the repository's
mainand records the exact installed commit.
LIMITS.md documents what PHARN does not guarantee.
THREAT-MODEL.md documents the attack surface and trust assumptions.
The architecture is specified in four documents:
pharn/CONSTITUTION.md— the principles that govern commands, rules, guarantees, and PHARN's own development process.pharn/ARCHITECTURE.md— the floor, primitives, layer tree, and pipeline.THREAT-MODEL.md— the security foundation and attack surface.LIMITS.md— what PHARN does not guarantee.
These trusted docs are protected from edits through Claude Code's Write/Edit/MultiEdit/NotebookEdit tool
surface. Bash writes are outside that protection (see Current limitations). All
four are copied into an install: the first two under pharn/, the other two at the project root.
PHARN is small-surface on purpose: a rule or enforcer is added in response to a real failure, not merely because a hypothetical checker could exist.
See CONTRIBUTING.md for the read-first order, required gates, and development loop.
Conduct expectations live in CODE_OF_CONDUCT.md; release history is in
CHANGELOG.md.
The floor and hooks carry zero runtime dependencies beyond the Node standard library; ESLint, Prettier, and markdownlint are development-only.
Found a vulnerability? Please follow SECURITY.md rather than opening a public issue.