# Session schema — the single format authority

Load when: bootstrapping a session, writing any working-surface file, dispatching the twin, or
interpreting detector output. The detector (`scripts/detector.mjs`) is the mechanical half of this
contract — every rule below that carries a rule ID is machine-checked; everything else is the
twin's semantic judgment.

## Folder layout

```
docs/research/{topic}-brainstorm/     ← open session (kebab-case topic slug)
├── state.md                          ← anchor + aspect map (twin-written)
├── thesis.md                         ← composed claim → argument → what changed (twin-written)
├── forks.md                          ← open questions with close conditions (twin-written)
├── ledger.md                         ← commitments (twin-written)
├── basis/                            ← frozen entry material (facilitator-written once, at intake)
├── fragments/                        ← harvest-native fragments (twin-written; materialize on
│                                        first real content, drafted in destination format)
└── ops/                              ← untracked capture layer (deleted at stop)
    ├── .gitignore                    ← contains `*` — ops/ never enters git      [BS-OPS-01/02]
    ├── capture.md                    ← facilitator's raw append ledger (its ONLY write)
    ├── sync-log.md                   ← paired twin-dispatch log
    └── manifest.md                   ← harvest manifest (created by the stop pipeline)
```

On a passed harvest the folder **moves** to `docs/history/brainstorms/{topic}/` (ops/ deleted
first) — the move is the completion marker; the user then deletes or commits it. A folder still
under `docs/research/` is by definition unfinished.

**Write protocol (one writer per file):** the facilitator appends to `ops/capture.md` and writes
the initial `basis/` copy plus the sync-log *dispatch* lines — nothing else, ever. The twin writes
every other file, at gates. The user may hand-edit anything; the twin diffs each file against its
own last compose before rewriting and folds foreign edits as first-class user signal.

## state.md

```markdown
# State

- Status: open | stopping | harvested
- Posture: mid-probe on the sync-layer aspect, three forks open
- Started: 2026-07-08
- Sitting: single | multi
- Last compose: 2026-07-08T14:30Z | never

## Aspect map

- persistence — how session state survives restarts → F1, D3
- onboarding — the first-run experience → thesis
- risks — failure modes and mitigations → none yet
```

- `Status` transitions only forward: `open → stopping → harvested` (the twin flips `stopping` when
  the stop pipeline begins, `harvested` at the move). [BS-STATE-01]
- The aspect map is derived per idea from the basis and edited by the user. Each aspect carries
  `→` refs to the content covering it: fork IDs (`F#`), ledger IDs (`D#`), `thesis`,
  `fragment:{name}`, or `none yet` (an open coverage gap — informational mid-session
  [BS-ASPECT-01], blocking at the manifest gate [BS-ASPECT-02]). Ref *presence* is the detector's
  half; whether the content actually covers the aspect is the twin's.

## thesis.md

```markdown
# {Topic} — thesis

Current as of: 2026-07-08T14:30Z

## Claim
## Argument
## What changed
```

Composed prose, rewritten whole at every compose, never appended. The three sections and the
stamp are mandatory [BS-THESIS-01]; the stamp must equal state.md's `Last compose`
[BS-COMPOSE-01]. At the manifest gate, `Last compose` must be later than the newest
`ops/capture.md` entry — a compose over unsynced capture is stale [BS-COMPOSE-02].

## forks.md

```markdown
## F3 — Should reads wait for a compose? [open]

Full self-contained prose in the user's framing.

- Raised by: user | twin (S4) | probe | harvest
- Closes when: a pick between wait-for-fresh and read-stale-with-warning
```

- IDs `F1, F2, …` unique, never reused [BS-FORK-01]. Status marker: `[open]` or
  `[parked → {destination}]`. Every fork carries `Closes when:` [BS-FORK-01].
- `Raised by: twin (S#)` must cite a reconciled sync-log entry [BS-SYNC-02].
- **A resolved fork's entry is deleted** by the twin — the resolving ledger entry's `Resolves:`
  field is the record. forks.md holds only open and parked forks; a ledger `Resolves: F3`
  coexisting with an open F3 is an inconsistency [BS-FORK-02]. Parked forks survive to the
  manifest, which must route each one [BS-MANIFEST-01].
- The manifest gate requires zero `[open]` forks [BS-FORK-03].

## ledger.md

```markdown
## D4 — Sessions initialize in dump or interview

- Decision: every session opens in dump or interview; probe and converge are mid-session moves.
- Why: a first-time user cannot be asked to pick a posture they have no vocabulary for.
  Declined: free initial posture — observed cold-starts stall in probe; re-evaluate if
  session-replay evals show experienced users fighting the default.
- How to apply: the facilitator's start calibration offers only the two entry moves.
- Resolves: F2
```

- IDs `D1, D2, …` unique, append-only [BS-LEDGER-01]. `Decision`, `Why`, `How to apply` are
  mandatory; `Resolves:` when the commitment closes forks. Amendments and reversals are
  annotations plus a new entry ("Reversed by D9" on the old; D9 names what it reverses) — never a
  rewrite. Declined options live inside `Why` per [doctrine.md](doctrine.md).

## ops/capture.md

The facilitator's per-turn append — its one write duty. Lowest possible bar: raw, unpolished,
everything session-relevant from *both* sides (user substance and facilitator substance —
diagnoses, informed rejections, definitions, reasoning chains), including residue judged not
worth pillar filing. Format:

```markdown
---
### T14 · 2026-07-08T14:22Z

<raw notes for this turn>
```

Turn IDs `T#` ascending with an ISO timestamp — the timestamp is what compose-freshness checks
against [BS-COMPOSE-02]. Never distill here; distillation is the twin's job.

## ops/sync-log.md

Paired-written, one pair per twin dispatch:

```
S4 | gate: on-demand | dispatched: 2026-07-08T14:25Z | scope: full reconcile + compose
S4 → reconciled | 2026-07-08T14:29Z | 4 edits · 2 new forks · 6 open · untouched: risks
```

The **facilitator** writes the dispatch line as it dispatches; the **twin** appends the result
line (`reconciled` or `failed`) on return — no single author holds both sides. Every dispatch
line must acquire a result line; only the newest entry may be unpaired (in flight) [BS-SYNC-01].
A `failed` result is surfaced to the user at the next turn break and re-run at the next gate.

## ops/manifest.md

Created by the stop pipeline; gates the harvest apply:

```markdown
# Harvest manifest — {topic}

Round: 1
Status: proposed | approved | applied

| # | What | Source | Destination | Action |
|---|------|--------|-------------|--------|
| 1 | consolidated source of truth | thesis + fragments + ledger | docs/features/{x}.md | create |
| 2 | parked fork F7 | forks.md | docs/architecture/open-questions.md | append |

## Uselessness test

- Result: pass | fail
- Cold-reader allowed paths: <the destination docs above>
- Gaps: none | <named gaps → next Round>
```

Every parked fork and every fragment must have a row [BS-MANIFEST-01]. `Status: approved` is set
only after the user approves; gap fixes re-enter as a new `Round` with the same approval
requirement. After apply, no destination doc may link into the session folder [BS-HARVEST-01];
the move to history requires a recorded passing uselessness test [BS-USELESS-01].

## Detector

`node scripts/detector.mjs <session-folder> [--gate=session|manifest|move] [--json]`

- `session` (default) — the always-on integrity rules; run at any gate.
- `manifest` — adds the convergence gates (zero open forks, full aspect coverage, fresh compose);
  the twin may not construct a manifest while any of these fail.
- `move` — adds the post-apply gates (manifest applied, uselessness pass recorded, no links into
  scaffolding); the folder may not move while any fail.

Exit code 0 = clean, 1 = violations (listed with rule IDs), 2 = unusable input. A run always
prints output — per-rule lines plus a `clean`/`failing` summary (or a usage/error message);
**exit 0 with empty output means the detector never executed** and is a failure, never a pass. Rules degrade
gracefully: no git context ⇒ BS-OPS-02 reports `skipped`, never blocks. Rule IDs are stable —
consumers (the twin, the evals, the maintenance census) key on them.
