ways
Concepts

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.

KindLocationWritten byRead by
state<worktree>/.ways/state.jsonevery way run — gated or analyticalthe way (to resume), the board
inventoryways.lock (repo root, or ~/.ways/ global)ways add / ways installways 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:

FieldAnswersExampleAnalogy
waywhich packaged artifact is runningincu/devthe program
flowwhich kind of gated processfeature · bug · securitythe mode you ran it in
disciplinewhich SDLC stage it belongs todevelopmentthe 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.lock sits 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.json path.
  • 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 — the inventory lock — omits timestamps on purpose, so two concurrent ways adds never churn or conflict on a clock value.
  • Enum enforcement. Every enumerated field (flow, phase, statuses…) is a schema enum, never a free string — so a typo like fature fails 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:

  1. Open vocabulary. flow and phase accept the known values or a namespaced <discipline>/<name> term (qa/regression, design/critique) — still catching typos while leaving the door open for new stages.
  2. Reserved ext object. State (and each gate/step) accepts a free-form ext — 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.md in the ways repo, with the state.schema.json shipped in the package.

On this page