State files
One state envelope, two typed kinds — where a way records where it is so it can resume and be inspected.
A state file is any file a way persists to know where it is (so it can resume) and to let tooling see what's going on. Before the standard these diverged in location, versioning, and rigor; now they share one envelope and two typed kinds.
| Kind | Location | Written by | Read by |
|---|---|---|---|
state | <worktree>/.ways/state.json | every way run — gated or analytical | the way (to resume), the board |
inventory | ways.lock (repo root, or ~/.ways/ global) | ways add / ways install | ways list / doctor / remove |
<worktree> is the git worktree root, or the repo root when no worktree is used.
One state file per worktree
There is exactly one state.json per worktree/branch — that is the state for that worktree.
No per-item files, no repo-level session file. Within a worktree, state.json answers "what am I
doing here"; across worktrees, git branch --show-current names where you are. Git already provides
the current pointer (HEAD); ways doesn't reinvent it.
This retires the earlier per-item .incu-way/items/{slug}.json model (schemaVersion 2). A new
flow starting in a worktree overwrites the current state.json — the report and git history
persist; only the progress pointer is replaced.
{
"schemaVersion": 2,
"updatedAt": "2026-06-30T00:00:00Z",
"flow": "feature",
"way": "incu/dev",
"wayVersion": "1.2.0",
"discipline": "development",
"slug": "003-contact-export",
"branch": "feat/003-contact-export",
"worktree": ".worktrees/003-contact-export",
"isolationType": "worktree",
"lastPhase": "prd",
"phase": "plan",
"documents": [
{ "name": "PRD.md", "path": "docs/prds/003-contact-export/PRD.md", "status": "approved" },
{ "name": "PLAN.md", "path": "docs/prds/003-contact-export/PLAN.md", "status": "in-review" }
],
"gates": [
{ "id": "prd", "label": "PRD review", "status": "passed", "at": "2026-06-30T00:00:00Z" },
{ "id": "plan", "label": "Plan review", "status": "pending", "at": null }
]
}Required: schemaVersion, flow, slug, branch, phase. Everything else is optional.
Three orthogonal axes: way · flow · discipline
Three fields are easy to conflate but describe different things — keeping them separate is what makes state both reproducible and classifiable:
| Field | Answers | Example | Analogy |
|---|---|---|---|
way | which packaged artifact is running | incu/dev | the program |
flow | which kind of gated process | feature · bug · security | the mode you ran it in |
discipline | which SDLC stage it belongs to | development | the department |
They're orthogonal: the same flow (feature) can be implemented by different way packages, and
one discipline (development) spans several flows. way and discipline are optional — they
earn their place in multi-way projects and for board grouping.
Analytical work is just another flow
An assessment, threat model, or docs run isn't a separate kind — it's a state with an
analytical flow (namespaced, e.g. architecture/assessment, security/threat-model), its
report recorded as a documents[] entry, and its stages as phase. It may run on its own branch
or without one (overwriting the worktree's current state.json). The report lives in docs/ and
persists regardless of the state pointer.
Shared conventions
- Canonical root
.ways/. All way state lives under<root>/.ways/;ways.locksits at the repo root (or~/.ways/global) for visibility but is part of the same family. - Format. UTF-8 JSON, 2-space indent, LF, one trailing newline, and keys in a stable order —
which kills diff churn and minimizes merge conflicts on the shared
state.jsonpath. schemaVersion— an integer ≥ 1 on every state file (the lock included), bumped only on a breaking shape change.- Timestamps, by rule. Progress state carries
updatedAt(ISO-8601 UTC), refreshed on every change. Merge-stable state — theinventorylock — omits timestamps on purpose, so two concurrentways adds never churn or conflict on a clock value. - Enum enforcement. Every enumerated field (
flow,phase, statuses…) is a schemaenum, never a free string — so a typo likefaturefails validation instead of silently persisting.
Extensibility & migration
ways ships the development profile today (feature / bug / security), but the rest of the
SDLC can plug in without a breaking change, two additive ways:
- Open vocabulary.
flowandphaseaccept the known values or a namespaced<discipline>/<name>term (qa/regression,design/critique) — still catching typos while leaving the door open for new stages. - Reserved
extobject. State (and each gate/step) accepts a free-formext— namespace your keys by vendor/discipline (ext: { "acme": { … } }). The core ignores it.
Migration is lazy and non-destructive: writers always write .ways/; a reader that finds no
.ways/state.json falls back to the legacy .incu-way/state.json (and older items/*.json), so
existing worktrees keep resuming. There's no migrate command — a worktree migrates itself on its
next write.
Normative source:
docs/standards/state-files.mdin thewaysrepo, with thestate.schema.jsonshipped in the package.