# okstra — Architecture and Operations Manual

> This document supplements [README.md](../README.md). See the README for a quick start, and this document for internal behavior, contracts, and the workflow.
>
> CLI arguments/options and the interactive input flow are documented separately in [cli.md](cli.md).

---

## At a glance

`okstra` is a **host-aware task bundle preparation tool** for multi-provider cross-verification. It is not a single-file review tool. Instead, it organizes task briefs, profiles, prompts, run history, and project-level discovery metadata around a stable task key so the current host-native lead can orchestrate provider workers consistently.

Its core capabilities at a glance are:

- **Task identity**: Creates or reuses a task root using a stable task key based on `<project-id>/<task-group>/<task-id>`, and consistently updates the manifest, index, and timeline.
- **Profiles by task type**: Loads standard task-type profiles such as `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, `final-verification`, and `release-handoff` to render the instruction set.
- **Run lifecycle**: Non-stage runs use `runs/<task-type>/` as the run directory, while `implementation` and single-stage `final-verification` use `runs/<task-type>/stage-<N>/`. Manifests, prompts, state, reports, sessions, worker results, and logs accumulate beneath the resolved run directory, and the filename suffix `-<task-type>-<seq>` separates reruns of the same phase.
- **Single python authority**: All prepare wiring—resolving profiles/workers/models, computing paths, rendering, and central record_start—is concentrated in a single function, [`okstra_ctl.run.prepare_task_bundle()`](../scripts/okstra_ctl/run.py). `okstra.sh` and the `okstra-run` skill are thin callers of that same function and do not pass state through environment variables. Task identity, paths, and workflow state are recalculated from authoritative on-disk files every time.
- **Host-aware handoff**: Claude Code, Codex, Antigravity, Grok, and Kimi can keep their current native session as the lead. The standalone compatibility launcher still starts a new `claude` process by default, while the external adapter uses registered CLI wrappers. Every path consumes the same `prepare_task_bundle` outputs.
- **Required team contract**: The `Required workers:` block in each phase profile is authoritative for the roster. General analysis phases use Claude/Codex analysers plus a report writer by default, while Antigravity, Grok, and Kimi are included only when allowed by both the profile and `--workers`. Lead-oriented phases such as `release-handoff` have separate rosters.
- **User-home install + project-local task bundles**: One `npx okstra@latest install` command installs the runtime (`~/.okstra/{lib/python, bin, templates, prompts}`) and installs public skills to `~/.agents/skills/` by default. If `~/.claude` exists, it also installs Claude skills and six worker agent definitions (`~/.claude/agents/*-worker.md`). Only user entry-point skills are exposed in the skill list; lead/support operating contracts are installed as runtime resources under `~/.okstra/prompts/` and are not discoverable as skills. Global conversation memory is stored separately from projects under `~/.okstra/memory-book/`. Task bundles and discovery metadata are stored under `.okstra/` in the target project. **In addition, `<PROJECT_ROOT>/.claude/settings.local.json` is provisioned as a symlink to `~/.okstra/templates/settings.local.json`** (`okstra setup` or `okstra-ctl` prepare manages it idempotently; if a regular file already existed, it is preserved as `.bak.<timestamp>` before replacement).
- **Resume and clarification**: Supports resuming the same task and responding to follow-up questions from the lead through `--task-key`, `--resume-clarification`, and `--clarification-response`.
- **Dual-audience derived views and telemetry**: For schema v2, derives the full reading copy Markdown (on demand) and task-specific human-facing HTML independently from the same v2 data.json. Schema v1 and quick Markdown reports keep their compatibility renderer. Worker error sidecars, wrapper log sidecars, and token-usage/cost accounting remain separate audit inputs.

The host-native Okstra lead owns judgment policy and worker orchestration. `okstra` prepares one runtime-neutral task bundle and resolves every provider assignment to either the current native session or a registered CLI wrapper.

## Table of contents

- [Purpose](#purpose)
- [What okstra does](#what-okstra-does)
- [Runtime assets vs support assets](#runtime-assets-vs-support-assets)
- [Architecture: python authority + thin callers](#architecture-python-authority--thin-callers)
  - [JSON ownership and model-facing projections](#json-ownership-and-model-facing-projections)
- [Host runtime execution behavior](#host-runtime-execution-behavior)
- [Lead prompt contract](#lead-prompt-contract)
- [Required team contract](#required-team-contract)
- [Stable task identity](#stable-task-identity)
- [Project self-registration](#project-self-registration)
- [Artifact-home rule](#artifact-home-rule)
- [Task type](#task-type)
  - [Standard task types](#standard-task-types)
  - [Information transfer between phases](#information-transfer-between-phases)
- (See [cli.md](cli.md) for CLI arguments, options, and interactive input)
- [Storage model & contracts](#storage-model--contracts) → [`architecture/storage-model.md`](architecture/storage-model.md)
  - Stable task root / per-run artifacts / `~/.okstra` indexes
  - Task manifest · task index · run manifest · timeline · runtime-neutral operating contract
- [Task brief usage](#task-brief-usage)
- [Recommended workflow](#recommended-workflow)
  - [1. Write a draft](#1-write-a-draft)
  - [2. Write a formal brief](#2-write-a-formal-brief)
  - [2.5. Triage requirements if needed](#25-triage-requirements-if-needed)
  - [3. Render-only validation](#3-render-only-validation)
  - [4. Run Claude](#4-run-claude)
  - [5. Analyze errors if needed](#5-analyze-errors-if-needed)
  - [5.5. Rerun immediately after answering](#55-rerun-immediately-after-answering)
  - [6. Review the implementation plan if needed](#6-review-the-implementation-plan-if-needed)
  - [7. Implement from the approved plan](#7-implement-from-the-approved-plan)
  - [8. Run the final review after implementation](#8-run-the-final-review-after-implementation)
  - [9. Resume the same task](#9-resume-the-same-task)
- [Lifecycle status and resume](#lifecycle-status-and-resume)
- [Final report structure](#final-report-structure)
- [Final report views (HTML)](#final-report-views-html)
- [Worker error collection (optional sidecar)](#worker-error-collection-optional-sidecar)
- [Token usage and cost accounting](#token-usage-and-cost-accounting)
- [Validators](#validators)
- [Practical notes](#practical-notes)
- [Related documents](#related-documents)

## Purpose

This document is a task-key-oriented operations guide to using `okstra` in `Okstra`.
Where `README.md` is the quick entry point, this document is the detailed reference for `okstra`'s execution contract, storage model, lifecycle, and host handoff rules.

`okstra` is not a single-file review tool.
`okstra` is a supporting tool that prepares stable task bundles, run history, and project-level discovery metadata so a supported host can perform cross-verification.

## What okstra does

okstra's prepare responsibilities are consolidated in a single Python entry point, [`okstra_ctl.run.prepare_task_bundle`](../scripts/okstra_ctl/run.py). This function performs the following as one transaction:

- Verifies that okstra installation assets exist (`~/.agents/skills/okstra-*`, optional `~/.claude/skills/okstra-*` / `~/.claude/agents/*-worker.md`, `~/.okstra/bin/...`)
- Self-registers `<PROJECT_ROOT>/.okstra/project.json` (or verifies that the projectId matches)
- Loads task type → `prompts/profiles/<task-type>.md` and extracts recommended workers
- Normalizes user worker/model overrides through the provider registry (Claude, Codex, Antigravity, Grok, Kimi, and report-writer-capable providers)
- Resolves task-brief / clarification-response paths (cwd first → PROJECT_ROOT fallback)
- Computes the stable task root and all paths/sequences inside a per-task mutex (`~/.okstra/.locks/<task-key>.lock`), then persists them to `<run-dir>/manifests/run-context-<seq>.json`
- Persists user input to `<run-dir>/manifests/run-inputs-<seq>.json`
- Renders the instruction set (`analysis-profile.md`, `analysis-packet.md`, `analysis-material.md`, `task-brief.md`, `reference-expectations.md`, `final-report-template.md`, `final-report-schema.json`, optional `clarification-response.md`, optional `directive.txt`, final-verification-only `verification-target.md`, canonical `lead-execution-prompt.md`, and its Claude-named compatibility alias) and writes a run prompt snapshot
- Updates `task-manifest.json`, `task-index.md`, `run-manifest-*.json`, `history/timeline.json`, and `discovery/{latest-task,task-catalog}.json`
- Writes a preassigned Claude session ID and `sessions/claude-resume-*.sh` (unless `--render-only` is used)
- Records record_start in the central indexes (`~/.okstra/{active,recent}.jsonl`, `projects/<id>/{index.jsonl, meta.json}`)

The two callers of `prepare_task_bundle` are:

1. **`scripts/okstra.sh`**: Parses and confirms CLI arguments → calls `prepare_task_bundle` → unless `--render-only` is used, launches `claude --model ... --session-id ... "$PROMPT"` via `exec`. It is a thin wrapper of about 160 lines.
2. **`okstra-run` skill**: Runs the [`okstra_ctl.wizard`](../scripts/okstra_ctl/wizard.py) state machine (`okstra wizard init|step|...` CLI) in the current supported host to collect user input → calls `okstra render-bundle` (that is, `prepare_task_bundle(render_only=True)`) → the current session reads the rendered lead prompt and assumes the lead role. It does not launch a new provider process for the native lead. The wizard decides all branching, validation, and ordering, so the skill body displays either the host's picker (`pick`) or a plain-text message (`text`) according to `Prompt.kind`.

The host-native Okstra lead owns judgment policy and worker orchestration. okstra's prepare stage only creates structured assets so that the lead starts with the correct input bundle and output skeleton.

Canonical roles are `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`, and `translator`. `lead` is a compatibility alias for `leader`. `executor` is a compatibility alias for `implementer`. New artifacts write only the canonical names.

Selection is role-first: `--role-count <role>=<N>` creates `RoleInstance` ordinals, `--role-model <role>=<modelRef>` and `modelDefaults` feed `ModelPool`, and a pinned model is kept only when the host can bind it exactly. The resulting `RoleExecution` owns `Invocation` and `Attempt` rows plus the stored `executionLabel`. Pane titles use that label. Shared Git object stores and other-stage refs are observed-projection only; they are not an audit enforcement surface.

## Runtime assets vs support assets

Runtime entry points are consolidated in Python packages. Bash and skills only call into them.

### Python module entry points (single authority)

> **Invocation contract.** The `python3 -m okstra_ctl.*` / `python3 -m okstra_project.*` forms below are **module identifiers** only; they are not installed into system site-packages. To invoke them directly from a shell, first export `PYTHONPATH` as `~/.okstra/lib/python`:
>
> ```bash
> eval "$(okstra paths --shell)"      # Export OKSTRA_PYTHONPATH and related variables
> export PYTHONPATH="$OKSTRA_PYTHONPATH"
> python3 -m okstra_ctl.run --help    # Now this works
> ```
>
> If those two lines are omitted, the command immediately fails with `ModuleNotFoundError: No module named 'okstra_ctl'` (a common pattern when an actual implementation-phase worker reads only the docs and invokes it directly). General users and workers should not invoke the modules directly; use the `scripts/okstra.sh` or `/okstra-run` entry point instead. Those wrappers configure PYTHONPATH automatically.

- [`okstra_ctl.run`](../scripts/okstra_ctl/run.py) — `prepare_task_bundle()` orchestrator + argparse CLI (`python3 -m okstra_ctl.run --workspace-root ... --project-root ... ...`, **PYTHONPATH must be configured—see the invocation contract above**).
- [`okstra_ctl.paths`](../scripts/okstra_ctl/paths.py) — pure path/sequence calculation in `compute_run_paths()`.
- [`okstra_ctl.run_context`](../scripts/okstra_ctl/run_context.py) — `compute_and_write_run_context()`, `write_run_inputs()`, and the per-task mutex.
- [`okstra_ctl.render`](../scripts/okstra_ctl/render.py) — task-manifest / run-manifest / timeline / task-index / team-state / launch.template / reference-expectations / discovery render functions + `python3 -m okstra_ctl.render <subcommand>` dispatcher (**PYTHONPATH must be configured—see the invocation contract above**).
- [`okstra_ctl.workers`](../scripts/okstra_ctl/workers.py) · [`okstra_ctl.models`](../scripts/okstra_ctl/models.py) — worker / model resolution.
- [`okstra_ctl.design_prep`](../scripts/okstra_ctl/design_prep.py) — implementation-planning design assessment fingerprints, deterministic request materialization, append-only confirmed inputs, and stage-scoped resolution. The Node `okstra design-prep` command is a thin caller of this module.
- [`okstra_ctl.workflow`](../scripts/okstra_ctl/workflow.py) — phase rules (PHASE_ALLOWED_OUTPUTS / PHASE_FORBIDDEN_ACTIONS).
- [`okstra_ctl.material`](../scripts/okstra_ctl/material.py) — `analysis-material.md` body + related-tasks builder.
- [`okstra_ctl.session`](../scripts/okstra_ctl/session.py) · [`okstra_ctl.seeding`](../scripts/okstra_ctl/seeding.py) — Claude session ID / resume command / installation validation / runtime settings.
- [`okstra_ctl.{ids,index,invocation,jsonl,project_meta,reconcile,resolver,sequence,batch,backfill,listing,locks,tmux}`](../scripts/okstra_ctl/) — existing central-index (`~/.okstra`) modules.
- [`okstra_project.{resolver,state}`](../scripts/okstra_project/) — PROJECT_ROOT resolution + project.json upsert + task-catalog/manifest reader.
- [`okstra_ctl.manager_cli`](../scripts/okstra_ctl/manager_cli.py), [`manager_store`](../scripts/okstra_ctl/manager_store.py), [`manager_sync`](../scripts/okstra_ctl/manager_sync.py), [`manager_launch`](../scripts/okstra_ctl/manager_launch.py), [`manager_paths`](../scripts/okstra_ctl/manager_paths.py) — cross-project manager state, one-way project snapshot sync, and child launch packet/context creation for `okstra manager`.

### Bash entry points (thin)

- [`scripts/okstra.sh`](../scripts/okstra.sh) — standalone compatibility launcher: CLI parsing / interactive prompt / `prepare_task_bundle` invocation / default `exec claude`. In-host Claude Code, Codex, and Antigravity runs use `okstra-run` and keep their current native session.
- [`scripts/lib/okstra/{cli,globals,interactive,project-resolver,usage}.sh`](../scripts/lib/okstra/) — CLI/interactive support only; contains no artifact-generation logic.
- [`scripts/okstra-ctl.sh`](../scripts/okstra-ctl.sh) + [`scripts/lib/okstra-ctl/`](../scripts/lib/okstra-ctl/) — central control-center CLI (list / show / open / rerun / reconcile / etc.).

### Runtime assets (templates + lead resources)

- `prompts/launch.template.md` — lead prompt template.
- `prompts/profiles/*.md` — eleven task-type profiles: the seven lifecycle profiles (`requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`) plus `improvement-discovery`, `project-analysis`, `feature-analysis`, and `change-impact-analysis` sidetracks.
- `templates/project-docs/task-index.template.md` · `templates/reports/final-report-v2.template.md` · `templates/reports/final-report-v2.template.md` · `templates/reports/html/` · `templates/reports/settings.template.json` — runtime render inputs. The unversioned Markdown template is the schema v1 compatibility template; the v2 Markdown and task-specific HTML trees are separate audiences.
- `<PROJECT_ROOT>/.okstra/project.json` — project self-registration. Created/verified automatically on the first okstra.sh run; when `--project-root` is omitted, PROJECT_ROOT is resolved through ancestors / `git toplevel`.

### Support assets (not referenced at runtime)

- `templates/reports/*-input.template.md` — aids for authoring user input.
- `validators/validate-workflow.sh`, `validators/validate-schedule.py`, `validators/validate-run.py` — for manual/CI validation. The path to `validate-run.py` is recorded in run metadata.

### Skills (`skills/`) and lead resources (`prompts/`)

- [`prompts/lead/okstra-lead-contract.md`](../prompts/lead/okstra-lead-contract.md) is the runtime-neutral lifecycle core: phase boundaries, artifacts, convergence, report ownership, and persistence semantics.
- `prompts/lead/adapters/claude-code.md`, `prompts/lead/adapters/codex.md`, `prompts/lead/adapters/antigravity.md`, and `prompts/lead/adapters/external.md` map the same semantic operations to one selected host runtime. The generated launch prompt exposes the core path plus exactly one adapter path.
- `prompts/lead/adapters/cmux.md` is selected by environment rather than by runtime: when the run manifest's `terminalBackend` is `cmux-pane`, every lead runtime resolves to it and dispatches through `okstra team`, because okstra owns the worker panes on that path instead of the host. It overrides only the adapter and the dispatch mode; the lead's agent, role, and session accounting still come from its own runtime.
- Runtime metadata and role assignments are persisted separately, but the lead provider is derived from the host: Claude Code maps to Claude, Codex maps to Codex, and Antigravity CLI maps to Antigravity. New runs persist `hostRuntime`, `leadAssignment`, and `workerAssignments[]`; each assignment records its provider, model, execution value, and resolved `native-session` or `cli-wrapper` runner. `lead-execution-prompt.md` is canonical, while `claude-execution-prompt.md` is a byte-identical compatibility alias for historical consumers.
- Host and provider registries remain separate: the active registered host owns the native lead session, while non-native providers run through their registered CLI wrappers.
- [`skills/okstra-setup/SKILL.md`](../skills/okstra-setup/SKILL.md) — **first-run bootstrap**. Runs `okstra install` and creates `project.json`.
- [`skills/okstra-run/SKILL.md`](../skills/okstra-run/SKILL.md) — host-aware in-session entry point that starts an okstra task in the current registered host session and calls `prepare_task_bundle` directly.
- Fourteen skills are user-invocable: `skills/okstra-setup/SKILL.md`, `skills/okstra-brief-gen/SKILL.md`, `skills/okstra-run/SKILL.md`, `skills/okstra-manager/SKILL.md`, `skills/okstra-memory/SKILL.md`, `skills/okstra-chat/SKILL.md`, `skills/okstra-inspect/SKILL.md`, `skills/okstra-rollup/SKILL.md`, `skills/okstra-usage/SKILL.md`, `skills/okstra-schedule-gen/SKILL.md`, `skills/okstra-container-build/SKILL.md`, `skills/okstra-pr-gen/SKILL.md`, `skills/okstra-user-response/SKILL.md`, and `skills/okstra-code-review/SKILL.md`. Only these are copied into the agent skill home. They cover brief authoring, phase execution, cross-project manager task coordination, global Memory Book storage/search, global chat rooms, read-side status/history/report/time/logs/cost/errors/recap, task-group-level aggregation of run results (rollup), project-wide historical resource usage, schedule support, local container deployment, PR description generation, clarification-response submission, and census-based code review of a stage or branch diff. `okstra-manager` uses `okstra manager` CLI JSON/launch packets as the source of truth, and stores manager-owned plans, assignments, directives, snapshots, and events under `~/.okstra/managers/<manager-id>/`. `okstra-rollup` is a read-side layer that fans the single-task aggregators from `okstra-inspect` (time/errors/recap) out to a task group or the whole project catalog. The `okstra rollup` CLI owns deterministic aggregation, while the skill (LLM) writes only the synthesized report summary. `okstra-usage` is a separate read-only resource snapshot grouped by lifecycle task type; it does not replace single-task `okstra-inspect` detail or the status/report digest from `okstra-rollup`. The canonical definition of `okstra-inspect` read-side facets is the subcommand table in `skills/okstra-inspect/SKILL.md`. `okstra-inspect logs` provides a read-only inventory and cleanup guidance for the live-log sidecars that the Codex/Antigravity wrappers write on every dispatch at the resolved `<run-dir>/prompts/<worker>-prompt-<phase>-<seq>.log`; for stage executions, the stage-qualified `run_dir` includes `stage-<N>/`. `okstra-inspect cost` summarizes `okstra context-cost`; `okstra-inspect errors` collects a task's okstra-run error logs into a timestamped Markdown error report and prints a summary; and `okstra-inspect recap` answers free-form questions about `.okstra` artifacts in addition to summarizing phases before and after each task run.
- Internal operating contracts—`context-loader` / `team-contract` / `convergence` / `report-writer` and the lead contract—have moved to `prompts/lead/*.md`. Language-specific coding preflight for implementation/verification workers has moved to `prompts/coding-preflight/*` (overview router + clean-code + three-stage language/framework/architecture selection). All are runtime resources installed under `~/.okstra/prompts/` and are not discoverable as skills. The generated launch prompt provides the lead with absolute paths, and reinstalling prunes the legacy exact-name skill directories `okstra-context-loader` / `okstra-team-contract` / `okstra-convergence` / `okstra-report-writer` / `okstra-coding-preflight` / `okstra`.
- Plugin manifest: [`../../.claude-plugin/plugin.json`](../.claude-plugin/plugin.json) — referenced by the supplementary `npx skills@latest add Devonshin/okstra` channel. Use `npx okstra@latest install` for normal setup. The plugin manifest exposes only the fourteen user entry points (`okstra-setup`, `okstra-brief-gen`, `okstra-run`, `okstra-manager`, `okstra-memory`, `okstra-chat`, `okstra-inspect`, `okstra-rollup`, `okstra-usage`, `okstra-schedule-gen`, `okstra-container-build`, `okstra-pr-gen`, `okstra-user-response`, `okstra-code-review`).
- Installation location: `~/.claude/skills/<name>/SKILL.md` or `~/.agents/skills/<name>/SKILL.md`.
- Release procedure: [`../../RELEASING.md`](../RELEASING.md) — npm publish flow and release-please / manual fallback.

## Architecture: python authority + thin callers

okstra's prepare stage follows an on-disk authority + single Python entry-point model. This design satisfies two goals simultaneously.

1. **Compatibility with parallel Claude Code execution**: Even if the same Claude session launches multiple children through subagents, parallel Bash tools, or background work, environment variables are not mutated across them, so no race occurs.
2. **Behavioral equivalence between the bash CLI and the in-session skill**: Because `okstra.sh` and the `okstra-run` skill call the same `prepare_task_bundle()`, their artifacts, central-index registration, and validation paths are identical.

```
┌────────────────────────────────────────────────────────────────┐
│ Two callers, one authority                                     │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│ scripts/okstra.sh         skills/okstra-run/SKILL.md           │
│ (CLI: parse bash args)    (okstra_ctl.wizard state loop)       │
│      │                              │                          │
│      └─────────────┬────────────────┘                          │
│                    ▼                                           │
│        okstra_ctl.run.prepare_task_bundle()                    │
│        (single Python function owns every artifact)            │
│                    │                                           │
│       ┌────────────┴────────────────┐                          │
│       ▼                             ▼                          │
│  on-disk authority             ~/.okstra (central index)       │
│  <PROJECT_ROOT>/.okstra/        per-task mutex + record_start  │
└────────────────────────────────────────────────────────────────┘
```

### JSON ownership and model-facing projections

Project-owned JSON is confined to `<PROJECT_ROOT>/.okstra/`; global indexes and installation metadata remain under `~/.okstra/`. An owned object is read through `okstra_ctl.json_boundary.load_owned_object`, which rejects missing files, invalid UTF-8, non-standard numeric constants, non-object top levels, schema failures, and cross-field failures. The boundary census in `tests/contract/test_json_boundary_census.py` rejects direct JSON parsing and generic atomic-write clones outside the dedicated boundary modules.

Owned-object writes validate the complete replacement value before publication. `write_owned_object_atomic` writes a temporary sibling, flushes and synchronizes the file, and uses atomic replace so a failed validation or partial write leaves the prior artifact in place. JSON Lines files use the dedicated `okstra_ctl.jsonl` append or rewrite operations because an append-only ledger has a different transaction contract from a replaced object.

External JSON enters only through a purpose-specific adapter that validates its producer lane and path policy. The supported exceptions are user-supplied files outside `.okstra`, worker result lanes, Claude Code configuration, external-tool output inside its workspace, legacy error sidecars, and convergence results beside their state file. The sealed `ExternalJsonSource` type prevents a caller from granting itself a generic exception.

Model callers receive fixed text projections instead of parsing owned objects. `okstra model-io` owns inspect projections, and `okstra preflight` renders named success or failure lines by default. A failure projection includes its stage, observed reason, and recovery action. Automation can request the unchanged preflight object with `--machine`; `--json` remains a deprecated one-release alias. A model-facing caller should rerun the named recovery action and then rerun the text command rather than opening the JSON artifact directly.

### State authority on disk

Task identity, paths, and workflow state are not stored in per-process environment variables. Every reader recalculates them from the following files:

| Data | Authoritative file |
|---|---|
| projectId, projectRoot, declared `architecture.style` | `<PROJECT_ROOT>/.okstra/project.json` |
| task identity / workflow | `<task-root>/task-manifest.json` |
| task candidate list | `<PROJECT_ROOT>/.okstra/discovery/task-catalog.json` |
| latest task pointer | `<PROJECT_ROOT>/.okstra/discovery/latest-task.json` |
| run inputs | `<run-dir>/manifests/run-inputs-<task-type>-<seq>.json` |
| run path hints / seq | `<run-dir>/manifests/run-context-<task-type>-<seq>.json` |
| approved implementation design assessment | `<implementation-planning-run>/reports/final-report-implementation-planning-<seq>.data.json` |
| AI-prepared design request | `<implementation-planning-run>/design-prep-requests/design-prep-request-<seq>-PREP-<NNN>.md` |
| confirmed design input | `<implementation-planning-run>/design-prep-inputs/design-prep-input-<seq>-PREP-<NNN>-r<revision>-<uuid>.md` |
| run history | `<task-root>/history/timeline.json` |
| global indexes | `~/.okstra/{active,recent}.jsonl`, `~/.okstra/projects/<id>/{index.jsonl, meta.json}` |

### Implementation design-preparation flow

Planning owns the immutable assessment, while Okstra and the user own separate sidecars. This separation lets the AI prepare a concrete proposal before asking for input without turning a later response into an edit of the approved report.

```text
implementation-planning Phase 6
  └─ final-report *.data.json
       ├─ ready / provisional / blocked / not-applicable
       └─ mode=no-design-inputs for a plan with no detected design surface
                    │
                    ▼ Phase 7
       design-prep-requests/ (Okstra-owned, deterministic)
                    │
                    ▼ okstra design-prep or wizard confirmation
       design-prep-inputs/ (user-owned, append-only)
                    │
                    ▼ implementation stage preflight
       proceed ── inject effective proposal and working assumptions
       wait_for_input ── stop before stage worktree and consumer creation
       replan ── stop before stage worktree and rerun planning
```

Resolution is stage-scoped: only items whose `stageRefs` include the selected stage participate, so an unsafe open decision blocks that stage rather than unrelated ready stages. `no-design-inputs` resolves to `proceed`. Plans created before the `implementation-design-prep-v1` report contract also proceed with a `legacy-unassessed` warning and are not rewritten.

### Fix-run incremental reverification

When an implementation run is prepared on a stage whose latest final-report data.json carries one or more verifier `FAIL` verdicts, prep derives a fix-run carry (`okstra_ctl.stage_fix_carry`) — previous report path, previous run HEAD, failed verifiers, carried blocking findings, routing recommendation — and injects it into the rendered analysis profile as a "Fix-Run Carry" block via the `{{FIX_RUN_CONTEXT}}` token. Verifiers then keep the full validation-command re-run but narrow the static design/test-quality sweep to `git diff <prev-head>..HEAD` plus a mandatory re-check of each carried finding. The report writer uses the prior report as read context and updates only the changed narrative blocks; Phase 7 recreates machine-owned fields from their current inputs. A first run on a stage, or a rerun after a PASS, renders the token empty and behaves as before.

### Clarification & decision-capture discipline

okstra runs a single cross-phase discipline for turning open questions into resolved decisions and durable memory. It is not one module — it is enforced at three sites that share one vocabulary, deliberately split across an interactive layer and a headless layer:

- **Interactive sharpening (bounded)** — `skills/okstra-brief-gen/SKILL.md` Step 4 asks one question at a time, each carrying a recommended answer, resolving facts from the codebase before asking. It is intentionally a *bounded* pass, not a decision-making interview: brief is a pre-discovery artifact, so the deep decision walk is pushed downstream.
- **Headless decision-tree walk** — the `requirements-discovery` and `implementation-planning` profiles walk the decision tree one branch at a time, emitting a static `Clarification Items` table (recommended answer `(a)` + alternatives `(b) (c)`) rather than a live interview, because these phases are headless batch runs. Facts answerable by `Read` / `Grep` are resolved with `path:line` evidence and never escalated.
- **Decision capture** — resolved terminology becomes okstra memory at `<PROJECT_ROOT>/.okstra/glossary.md` (approval-gated, `okstra-brief-gen` Step 4.5). A decision is recorded as `<PROJECT_ROOT>/.okstra/decisions/<NNNN>-<slug>.md` only when it is hard to reverse, surprising without context, and the result of a real trade-off; `implementation-planning` is the sole owner of that evaluation and `validators/validate-run.py` enforces materialization.

When changing any one site, keep the shared vocabulary (recommended-answer, codebase-first, glossary, decision-record) aligned across all three so the discipline does not drift.

### Concurrency

Two types of file locks handle all concurrency.

- `~/.okstra/.locks/<task-key>.lock` — per-task mutex. `compute_and_write_run_context` combines sequence calculation and persistence of compact `run-context.json` into one transaction. Two calls for the same task are serialized.
- `~/.okstra/.lock` — central-index mutex. Used by `record_start` / `reconcile` / `rotate_recent_if_needed`.

There is no serialization between different (project, task-group, task-id) tuples because their disk paths are separate and cannot collide. With environment-variable races eliminated, one Claude session can safely run multiple tasks in parallel.

### Allowed env vars (user knobs only)

The following environment variables are read only as user settings, not for state transfer.

- `OKSTRA_HOME` — overrides the central directory location (default `~/.okstra`).
- `OKSTRA_DEFAULT_LEAD_MODEL`, `OKSTRA_DEFAULT_CLAUDE_MODEL`, `OKSTRA_DEFAULT_CODEX_MODEL`, `OKSTRA_DEFAULT_ANTIGRAVITY_MODEL`, `OKSTRA_DEFAULT_REPORT_WRITER_MODEL` — model defaults.
- `OKSTRA_TOOL_NAME`, `OKSTRA_COMMAND_NAME` — display names in usage output.
- `OKSTRA_RUN_SEQ_OVERRIDE` — run-sequence override forced by okstra-ctl rerun / test hooks (per-process).

Other variables such as `PROJECT_ID`, `TASK_GROUP`, `RUN_*`, `FINAL_*`, and `CLAUDE_*` are not exported and do not leak into child processes.

## Host runtime execution behavior

Host adapters and model providers are independent axes. The host registry discovers bundled adapters and explicit user installs under `~/.okstra/adapters/hosts/<id>/`; the provider registry uses the parallel `~/.okstra/adapters/providers/<id>/` root. Neither registry executes adapter code found in a project repository. A host descriptor names its native provider, while every non-native worker assignment resolves through the provider's CLI wrapper.

The terminal front door accepts `okstra run <host-id-or-alias>`, resolves it through `HostAdapterRegistry`, and probes `entry_mode="spawn-process"` readiness before starting that adapter's CLI. It does not infer the host from installed provider binaries and does not fall back to another host when the selected adapter is unavailable.

The legacy standalone shell path remains Claude-specific:

**Mode A — `okstra.sh` launches a new Claude process**
- With `--render-only`, it exits after creating the instruction set without running Claude.
- Without `--render-only`, the prepare stage preassigns a Claude session ID and creates `claude-resume-<task-type>-<seq>.sh` under the current run's `sessions/` directory.
- It then runs `claude --model <lead> --session-id "$CLAUDE_SESSION_ID" "$PROMPT"` via `exec` from the target project root using the standalone launcher's resolved Claude lead model. (The former `--settings <runtime-settings>` argument was removed in 0.14.0; permissions are now provided by the `<PROJECT_ROOT>/.claude/settings.local.json` symlink.)
- `okstra.sh` performs only this compatibility handoff; the launched session continues under the same Okstra lead contract. Codex-hosted runs do not pass through this shell path.

**Mode B — the `okstra-run` skill hands off within the current host session**
- Use this when the user is already in a registered host and wants to start a new okstra task there.
- The skill probes `entry_mode="current-session"`, reads the selected relay contract, and declares only the semantic functions the live harness actually exposes: `plain_text_input`, `native_single_select`, `native_multi_select`, and `native_question_group` as available. The wizard receives their intersection with the adapter contract instead of inferring capabilities from a host name.
- The skill relays the wizard through that semantic interaction plan and calls `prepare_task_bundle(render_only=True)` with the explicit host runtime.
- It does not launch another lead process. The current host-native session reads `lead-execution-prompt.md` and assumes the Okstra lead role.

Kimi and Grok implement this same lead contract. Their provider roles remain `lead`, `analyser`, and `critic`; they do not gain executor, verifier, or report-writer roles from being lead-capable hosts.

Both modes create identical artifacts (task-manifest, run-manifest, timeline, instruction set, and central-index registration), so subsequent `okstra-ctl` commands (list / show / rerun / reconcile) operate consistently without distinguishing between them.
- The handed-off native host acts as the Okstra lead, responsible for orchestration and final synthesis.
- The default worker policy remains Claude + Codex analysis and a Claude report writer. Antigravity, Grok, and Kimi are profile-gated optional assignments; Grok and Kimi are read-only analyser/critic providers.
- The lead assigns worker responsibilities and makes the final judgment after reading the task bundle.
- okstra Claude assets installed in the user home (`~/.claude/skills`, `~/.claude/agents`) instruct Claude to dispatch workers through `Agent(name: ...)`; workers automatically join the session's implicit team.
- **Team lifecycle (Claude Code v2.1.178+)**: v2.1.178 removed the `TeamCreate` / `TeamDelete` tools and the `team_name` parameter from `Agent(...)`. When `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` (seeded into `settings.json` by `okstra install`), one implicit team per session is created automatically at startup. In Phase 3, the lead does not call a team-creation tool. It records only the `teamName` audit label and `teamCreate: { attempted: false, status: "implicit", splitPane: <true-if-TMUX-is-set> }`, then dispatches workers through `Agent(name: "<role>-worker", run_in_background: true)` (without team_name). Split-pane teammates appear when `$TMUX` is set and `teammateMode: auto`; outside tmux they run in-process, and both modes are valid. At run end, after Phase 7 token accounting, the lead checks for remaining tmux panes and asks whether to clean up worker teammates only for split-pane runs. If approved, `okstra-team-reconcile.sh` marks dead-pane stale-active members inactive and sends each completed teammate a `SendMessage` shutdown_request. There is no tool for deleting the implicit team; it disappears when the session ends. If the user keeps the teammates, they remain in the FleetView roster and the lead tells the user to remove them through Teams/FleetView (see *Run-end teammate teardown* in `prompts/profiles/_common-contract.md`). Phase 7 token accounting locates worker sessions by top-level `agentName` or nested `subagents/agent-a<name>-<hash>.jsonl` filenames when `teamCreate.status` is `implicit`/`skipped`/`error`.

## Lead prompt contract

The canonical lead prompt body is rendered from `prompts/launch.template.md` to `lead-execution-prompt.md`. A selected runtime adapter maps its neutral operations to host primitives.

- Prompt substitution is limited to scalar placeholder values such as the task key, session ID, and absolute/relative paths.
- The selected profile body is rendered to `instruction-set/analysis-profile.md`.
- The analysis material body is rendered to `instruction-set/analysis-material.md`.
- The expected state for config/deployment is rendered to `instruction-set/reference-expectations.md`.
- The lead must read these artifact files directly; long task-specific bodies must not be duplicated inline in the launch prompt.

## Required team contract

The standard `okstra` workflow applies the following team contract consistently across runtime prompts, profiles, manifests, and skill documentation.

- The current host-native provider owns the synthesis-only lead session: Claude on Claude Code, Codex on Codex.
- Every selected worker comes from the profile roster and provider capability registry. The default analysis policy remains Claude + Codex + a report writer; Antigravity, Grok, and Kimi are attempted only when the profile and resolved roster include them.
- `Report writer worker` focuses on report structure and evidence organization, while the host-native lead remains the final synthesis owner. Claude is the default report-writer provider; Codex may be selected explicitly.
- Model defaults are provider and functional-role policy. Fallbacks include Claude lead/analyser=`opus`, Codex lead/analyser=`gpt-5.6-sol`, Claude report writer=`sonnet`, Antigravity=`gemini-3.1-pro`, Grok analyser=`grok-4.6`, and Kimi analyser=`kimi-k2.7-code`. Selectable catalog models may be assigned to every role.
- Before the final judgment, each required role in the current run's worker roster must have either a result or an explicit terminal status (`completed`, `timeout`, `error`, `not-run`).
- Every attempted worker (`completed`, `timeout`, `error`) must have an assigned worker prompt history file under the current run's `prompts/` directory.
- Worker timing begins at the atomic transition to `in-progress`, which records `workers[].startedAt` in `team-state.json`; prompt creation time is not a dispatch proxy. `okstra worker-state transition` and both dispatch adapters share `dispatch_state.transition_worker_status`, while `okstra worker-liveness --team-state ... --worker ...` reads that timestamp as the launch-grace authority for both probe kinds — the in-process audit sidecar is reused on re-dispatch, so only `startedAt` separates the previous attempt's last heartbeat from this dispatch's silence.
- An unnamed generic parallel worker is not accepted as a substitute for a required role.

### Cross-task worker prompt policy and final-verification boundaries

`PromptPlan` is the generating SSOT for functional prompt audience, equality group, packet-only input, coding-preflight eligibility, required headers, and size limits. It resolves only from task type, worker ID, the manifest's executor worker ID, and dispatch kind; provider and model identity never assign scope. The resolved analyser order used for improvement primary-pass rotation is a separate roster operation and is not a `PromptPlan` input.

The same analysis-core rule applies to `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `improvement-discovery`, and `final-verification`: every selected initial analyser receives the same normalized semantic body and independently covers the whole common scope. In `implementation`, the implementation executor is excluded from verifier equality; all selected implementation verifiers form their own equality group. If the roster contains one verifier, individual header rules still apply, while one verifier makes normalized equality a deliberate no-op. Report-writer and reverify prompts are excluded from analysis equality groups because they author or adjudicate existing findings instead of producing an initial independent analysis.

The policy selects one audience preamble: analysis uses `templates/worker-prompt-preamble.md`; executor and verifier use `templates/implementation-worker-preamble.md`; report writing uses `templates/report-writer-prompt-preamble.md`. Every initial audience also reads the shared `templates/worker-error-contract.md`. Only implementation executor/verifier prompts receive `**Coding preflight pack:**`; a report writer never loads implementation coding instructions.

`initial_prompt_materialization.py` is the sole owner of roster-derived initial prompt rendering, request compatibility checks, contract validation, and immutable publication. `dispatch_core.py` and `codex_dispatch.py` select workers and pass delivery capabilities; Phase 7 `validators/validate-run.py` revalidates the persisted artifacts through the same `worker_prompt_contract`. Report-writer prompts still receive individual common-anchor validation, while `-reverify-r<N>-` prompts keep their separate lightweight convergence contract.

Final-verification uses one shared full-core responsibility for every selected initial analysis worker. Provider/model diversity supplies independent observations of the same requirements; it does not split acceptance criteria into disjoint worker scopes. `analysis-packet.md` carries the worker-facing verification procedure, while report-only deliverable and self-review guidance stays with the report writer.

At final-verification entry, `prepare_task_bundle()` snapshots the resolved worktree, verification scope, base ref, head ref, implementation/stage report mapping, and diff stat into `instruction-set/verification-target.md`. It records the sidecar's project-relative path and `sha256:<hex>` digest in the task/run manifest and active-run context. Initial prompts carry only six compact target identity headers plus one `analysis-packet.md` path; workers open the target sidecar on demand. The coding-preflight anchor remains implementation-only, regardless of whether an initial pane is displayed with the role `verifier`.

All selected initial prompt bodies must normalize to the same semantic content. One optional shared `## Run-specific directive` is bounded to 40 nonblank lines, and larger material is placed in the instruction set. Final-verification adds compact target identity and size limits to the common cross-task policy.

### Deterministic convergence engine

`ConvergenceEngine` owns deterministic state transitions after Round 0 grouping: queue membership, roster-aware dispatch plans, vote reduction, classification, round history and limits, skip reasons, final state, and classification counts. The lead retains semantic grouping and evidence interpretation because those operations require judgment. Runtime adapters are transport-only: they dispatch the persisted batch and return structured terminal outcomes without recalculating engine state.

The complete artifact lifecycle is: worker results → Round 0 grouping → reducer queues only non-consensus findings → analyser-instance re-verification → optional critic gap reducer transition → report-writer narrative → deterministic plan-item extraction → plan-body verifier round → one final-report assembly → render/validation. Initial workers write their findings to the assigned Result Path and their reading confirmation to the generated `Audit sidecar path`; the sidecar is evidence of input reading, not a source of findings.

Cross-verification does not mean that worker A reviews worker B's entire result. Round 0 records multi-source agreement immediately, and the reducer asks independent analyser instances to vote only on single-source or still-unresolved findings selected in the persisted queue. The report writer is never a voter. It organizes the validated result, while the later plan-body round verifies the consolidated `P-*` plan items rather than reopening the `F-*` finding queue.

The lead writes the grouped input, then advances it through the internal admin CLI operations `okstra convergence seed`, `plan-round`, `apply-round`, optional `apply-critic-gaps`, `finalize`, and `validate`. For worker W, each generated dispatch excludes findings originating from W; resolved findings leave the queue permanently. Lightweight reverify receives only its current persisted batch and embedded evidence, not the original analysis packet, profile, brief, or instruction set. Its prompt carries an exact task type and active-phase forbidden-actions block; dispatch validates those phase anchors, and the run validator rejects a recorded phase-boundary violation. Terminal worker non-results and completed per-finding `UNVERIFIABLE` responses become `verification-error`; the engine never fabricates a `DISAGREE` vote. The report writer does not vote and consumes the validated terminal convergence state and every analysis-worker result as read context. Under report contract v3 it writes only the narrative Markdown, a pointer, and its audit sidecar. Approval decisions, activity, execution, convergence, design preparation, and plan-body verification remain in their single-owner inputs. Phase 7 validates those inputs and atomically publishes `data.json` once. The full reading copy is rendered on demand. Newly finalized convergence output is schema v1.3; under the compatibility path, valid historical final schema versions v1.0, v1.1, or v1.2 are reused and consumed without rewrite.

Coverage critic and plan-body verification remain separate from finding convergence. The critic audits the integrated Round 0 analysis, while implementation-planning's plan-body gate validates the later report draft through its own `P-*` queue and state file. Neither path changes the engine's `F-*` queue.

`okstra convergence` and `okstra plan-items` are lead-contract admin interfaces, not public skill entry points. The former `okstra-convergence` skill stays obsolete; these commands do not expand the installed public skill roster.

Seeding provides the migration boundary for in-progress runs. A valid terminal legacy final is reused unchanged. A matching valid working state resumes. A malformed or partial legacy state is archived byte-for-byte under `state/migrations/` before an explicit Round 0 restart; an invalid new-engine working state fails closed until `--restart-from-round0` is supplied. Replacement of an existing final is allowed only while its recorded archive still matches the original bytes.

## Stable task identity

`okstra` uses the following combination as its basic identifier:

- `project-id`
- `task-group`
- `task-id`

The logical task key has this form:

```text
<project-id>:<task-group>:<task-id>
```

Reuse the same `task-group` and `task-id` when reopening the same bug or continuing the same work.
Use a new `task-group` or `task-id` for unrelated work.

## Project self-registration

`okstra.sh` operates without an external registration directory (the former `examples/projects/*.conf.sh` model has been retired). At the start of each run, it resolves PROJECT_ROOT in the following order and self-registers `.okstra/project.json` at that location as the authoritative source.

Resolution order:

1. CLI argument `--project-root <path>`.
2. The location containing `.okstra/project.json` in cwd or one of its ancestor directories.
3. `git rev-parse --show-toplevel` from cwd.

If all three fail, `okstra.sh` exits immediately with an error (there is no automatic guess).

`<PROJECT_ROOT>/.okstra/project.json` schema:

```json
{
  "projectId": "sample-project-v2-api",
  "projectRoot": "/Volumes/Workspaces/workspace/projects/sample-project",
  "createdAt": "2026-05-10T00:00:00Z",
  "updatedAt": "2026-05-10T00:00:00Z",
  "worktreeSyncDirs": [".project-docs", ".scratch", "graphify-out", ".claude"],
  "architecture": { "style": "hexagonal" },
  "reviewRulePacks": ["/Users/me/.claude/skills/team-pr-reviewer/SKILL.md"]
}
```

On the first run, it writes the four fields `projectId`, `projectRoot`, `createdAt`, and `updatedAt`. If the file already exists, it verifies that the `--project-id` argument matches the stored `projectId`, then updates only `projectRoot`/`updatedAt`. Unknown user-added fields such as `worktreeSyncDirs` and future `mcpServers` are preserved during the upsert. A mismatch causes an immediate exit, preventing two IDs from being mixed in the same directory.

`worktreeSyncDirs` (optional) overrides, per project, the list of project-root-relative directories to symlink into task worktrees. Resolution order is the `OKSTRA_WORKTREE_SYNC_DIRS` environment variable → `project.json` → built-in default (`.project-docs`, `.scratch`, `graphify-out`, `.claude`). An empty array disables syncing entirely. Sync exists only for filesystem continuity; the okstra context/write boundary remains `<PROJECT_ROOT>/.okstra/**`.

`architecture.style` (optional, one of `hexagonal` / `layered` / `none`, default `none`) declares the project's architecture. It is read by [`scripts/okstra_project/resolver.py`](../scripts/okstra_project/resolver.py) `resolve_architecture`, which falls back to `none` on an absent field, an unrecognized value, or an unreadable `project.json`. It is the switch for the second of two enforcement layers.

**Layer 1 — always on, style-agnostic.** Independent of any declaration, every `implementation-planning` plan emits `variationPointAnalysis`: whether the same behavior is served by two or more implementations, and for each such point the interface it is extracted behind (`extractionDecision`) plus the Stage Map stage that builds it. `hasMultipleImplementations: false` is a claim rather than an omission, so it requires a written `noVariationRationale` and an empty `points` array. The recommended option carries `testSeams` — one row per boundary a test injects at and replaces — and each point becomes a `P-Var-<N>` item judged in the plan-body verification gate (§5.5.9), which DISAGREEs when an extraction branches on resource identity instead of extracting the interface the next implementation plugs into (the open/closed shape). **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.VariationPointAnalysis` pins the shape, [`validators/validate-run.py`](../validators/validate-run.py) `_validate_variation_point_analysis` rejects a rationale-less or points-carrying `false`, a point-less `true`, and an `extract: true` decision with an empty `interfaceKind` or `coveredBy`, and `okstra_ctl.plan_items` emits the `P-Var-*` items.

**Layer 2 — only when a style is declared.** A declared style promotes the placement rules from advisory to binding at three sites. `hexagonal`: extraction itself is not made mandatory, but when a point *is* extracted the interface has to be a port — an `extractionDecision` carrying `extract: true` with any `interfaceKind` other than `"port"` is a validator failure in the same `_validate_variation_point_analysis`, while `extract: false` remains a legal decision the style does not reject; the executor loads `prompts/coding-preflight/architectures/hexagonal.md` even when none of the router's Stage 3 layout signals matched, so the declaration, not the directory shape, decides; and the verifier promotes an added or modified service dependency injecting a concrete adapter instead of a port from an advisory recommendation to a blocking finding → verdict `FAIL` ([`prompts/profiles/_coding-conventions-preflight.md`](../prompts/profiles/_coding-conventions-preflight.md), [`_implementation-verifier.md`](../prompts/profiles/_implementation-verifier.md)). `layered` has no pack resource; its binding invariant is dependency direction — an upper layer may import a lower one, never the reverse — and it is worker judgement, because no machine check reads layer names.

`none` (or an absent field) keeps layer 1 only: the placement overlay stays advisory and Stage 3 stays detection-driven, so an already-configured project's behavior does not change until it opts in.

`reviewRulePacks` (optional, an array of absolute paths, default empty) declares the project's own review rule packs — the review standard a phase reads before it judges a plan or a diff, such as a team's PR-review skill. A pack used to reach a run only when the task brief cited its exact path, so whether the team standard applied depended on who wrote the brief; a declaration here applies to every run in the project, and the two channels are a union. It is read by [`scripts/okstra_project/resolver.py`](../scripts/okstra_project/resolver.py) `resolve_review_rule_packs`, which drops a relative entry — a worker's cwd is a worktree, where a relative path names a different file — and falls back to "none declared" on an unreadable or malformed `project.json`. Three phases consume it: `implementation-planning` (plan away the findings before code exists), the implementation executor's coding-conventions preflight, and the static review passes of the implementation verifier and `final-verification`. **Enforced:** `okstra doctor --phase <phase>` fails the `review rule packs` check when a declared path is not a readable file ([`scripts/okstra_ctl/doctor.py`](../scripts/okstra_ctl/doctor.py) `_review_rule_pack_check`), because a stale path otherwise costs the whole pack in silence. Whether a pack that *does* resolve was actually read stays the phase's own `project-review-rules:` record — no machine check reads a worker's reasoning.

The authoritative source for `okstra-ctl` reindex/backfill also changed under the new model. Previously it sourced `examples/projects/*.conf.sh`; it now scans `~/.okstra/projects/<projectId>/meta.json` (the mirror of the project.json information produced by record_start) to restore (projectId, projectRoot) mappings. The `OKSTRA_PROJECT_DEFINITION_DIR_OVERRIDE` environment variable has also been retired.

## Artifact-home rule

okstra has exactly one project artifact root: `<PROJECT_ROOT>/.okstra/`. Anything outside this root is not okstra memory. It may be read as read-only source material only when explicitly cited by the brief's `Source Material` or `Reporter Confirmations`; okstra does not write outside the root on its own judgment.

There is one exception: when the user requests, **verbatim**, that a specific non-okstra file be edited in the brief's `Source Material` or `Reporter Confirmations` section. The phase that performs the edit must include the user's original wording in its final report.

okstra maintains its own institutional memory inside its subtree.

- `<PROJECT_ROOT>/.okstra/glossary.md` — the okstra glossary accumulated across runs.
- `<PROJECT_ROOT>/.okstra/decisions/<NNNN>-<slug>.md` — okstra decision records. Candidates are evaluated during the `implementation-planning` phase; `okstra-brief-gen` only marks them as candidates.

okstra phases do not write PRD or issue files directly. Equivalent decision artifacts are created inside `.okstra/` by `requirements-discovery` and `implementation-planning`.

## Task type

`task-type` simultaneously determines the purpose of the current run, profile selection, and lifecycle phase routing.

Selection rules:

- With `--task-type <name>`, `prompts/profiles/<name>.md` is rendered as the task bundle's `instruction-set/analysis-profile.md`.
- The single selector in the external interface is `task-type`.
- The selected task type is reflected unchanged in `taskType` and `workflow.currentPhase` in task-manifest.json. It is not written into `workflow.nextRecommendedPhase`: prepare only lowers an inherited `ready` pointer to `pending`, and the pointer's next value comes from the report this run produces.
- It is also used as the run-directory path segment (`runs/<task-type>/...`).

### Standard task types

Each task type enforces phase-specific allowed and forbidden actions. A run creates only the artifacts for its own task type and does not advance to the next phase. The next phase always begins with a new `okstra.sh` execution.

The fourth column is the `workflow.nextRecommendedPhase` pointer Phase 7 leaves behind. The pointer is an object `{phase, status, rationale}`; `status` is `ready`, `pending`, `blocked`, or `terminal`, and `phase` names a phase only under `ready`. Phase 7 does not decide the route — it projects the pointer from the report's own routing field (`scripts/okstra_ctl/next_phase.py::project`), named per row below. The authoring rule for the field is stated once, in the Phase 6 checklist of [`prompts/lead/report-writer.md`](../prompts/lead/report-writer.md); this table describes the outcome, not the rule.

| task type | Purpose | Core artifacts | Next-phase pointer after Phase 7 | Code changes allowed? |
|---|---|---|---|---|
| `requirements-discovery` | Classify the request as bugfix, feature, refactor, ops, or improvement, then route it to a safe next phase | work category, routing decision, missing-input list, clarification requests | from `requirementsDiscovery.routing.nextTaskType`: `ready` at `error-analysis` or `implementation-option-selection`; `pending` when the run settles on neither | No |
| `error-analysis` | Analyze the symptoms, causes, and reproduction gaps of a reported error/incident based on evidence | symptom/trigger summary, root-cause hypotheses, reproduction gap, validation path | from `errorAnalysis.routing.nextTaskType`: `ready` at `implementation-option-selection` after a credible cause, or at `error-analysis` for continued investigation | No |
| `implementation-option-selection` | Compare or validate implementation directions before detailed planning | up to three ranked directions, per-direction `coveragePercent` and `scopePrecisionPercent`, rejected-candidate audit, separate `DIRECTION SELECTION` response | from the `implementationOptionSelection.routing` string enum: `ready` at `implementation-planning` once a direction is confirmed, `pending` on `pending-direction-selection`, `blocked` on `blocked` | No (strictly read-only; source edits, builds, tests, migrations, and deploys are prohibited) |
| `implementation-planning` | Expand one selected direction into an executable plan without changing its mechanism or architecture boundary | selected-direction snapshot/reference, direction realization, affected-file list, Stage Map, validation/rollback, exact plan coverage, YAML frontmatter `approved: false`, **§5.5.9 Plan Body Verification**. Existing plans without `planningContract: selected-direction` retain the legacy option-candidate and `implementation-option:` contract | from `implementationPlanning.outcome`: `ready` at `implementation` on `plan-ready` (the plan still needs its separate approval before that run starts), `ready` at `implementation-option-selection` on `direction-invalidated` | No |
| `implementation` | Modify source code according to the approved `implementation-planning` final report. **One run executes exactly one stage** (selected with `--stage <auto\|N>`) | commit list, diff summary, out-of-plan edits block, validation/TDD evidence, rollback verification, verifier results (Antigravity/Codex/Claude), `carry/stage-<N>.json` evidence sidecar | from `implementation.routingRecommendation.target`: `ready` at that phase — `final-verification` on a clean stage, otherwise `error-analysis`, `implementation-planning`, or `implementation` | Yes (limited to the approved plan's file list; `git push`/publish/deploy/real migration prohibited) |
| `final-verification` | Check completed work for residual defects and regression risk, then make a release judgment | acceptance verdict, residual risk, follow-up routing (`error-analysis`/`implementation-option-selection`/`implementation-planning`/`release-handoff`) | from `finalVerification.routingRecommendation.target`: `ready` at that value — `release-handoff` only on an `accepted` verdict, otherwise the phase owning the defect (cause, selected direction, or detailed plan). `release-handoff(stage-group)` is a scope qualifier on the same phase, so it projects to `release-handoff`. `done` becomes `terminal` | No (read-only tests only) |
| `release-handoff` | Deliver `accepted` changes as a commit, push, or PR according to the user's chosen method | user menu responses (H1 action / H2 PR base / H3 message handling), executed git/gh command log, commit SHA list, PR URL | always `terminal` — the lifecycle ends here and this report has no routing field Phase 7 projects from | Yes—but execute **only the mutating commands selected by the user in the menu**. `git push --force*`, direct push to the base branch, `--no-verify`, `gh release`, and publish/deploy are prohibited. The source code itself must not be changed; package the existing `implementation` diff unchanged. |
| `project-analysis` | Map the current project structure and feature index | components, dependencies, entry points, data stores, external systems, feature index | always `pending` with no phase — a sidetrack settles no route | No (strictly read-only; tests are also prohibited) |
| `feature-analysis` | Trace one confirmed existing feature | flows, domain rules, state changes, external interactions, test coverage | always `pending` with no phase — a sidetrack settles no route | No (strictly read-only; tests are also prohibited) |
| `change-impact-analysis` | Map the impact of one proposed change | preserved behavior, impact items, dependency blast radius, test and operational impact | always `pending` with no phase — a sidetrack settles no route | No (strictly read-only; tests are also prohibited) |

Common constraints:

- Every phase except `implementation` prohibits source-code edits, builds, migrations, deployments, and other state-mutating commands (`final-verification` allows read-only test commands only). `implementation` permits edits/commits only within the file list of the approved plan; `git push`, publish, deploy, real migration, and third-party write APIs remain prohibited.
- **Isolated worktree for pre-implementation non-implementation phases (BLOCKING)**: The first pre-implementation non-implementation phase prepare creates a task-key `git worktree` through `okstra-ctl`. Pre-implementation non-implementation phases reuse the task-key worktree: `requirements-discovery` → `error-analysis` → `implementation-option-selection` → `implementation-planning` use the same worktree and branch for the same task key. `implementation` does not reuse this task-key worktree; implementation uses a dedicated stage-specific worktree and branch for every stage/run, as described in the next item. The task-key worktree lives at `~/.okstra/worktrees/<project-id>/<task-group-segment>/<task-id-segment>/` (special characters such as `/` and `:` in segments are normalized to `-`), and the branch is named `<work-category-namespace>/<task-id-segment>` (for example, `feature/dev-9436` or `fix/dev-7311`). The namespace is derived from work_category (`feature`·`improvement`→`feature/`, `bugfix`→`fix/`, `refactor`→`refactor/`, `ops`→`ops/`, unspecified→`task/`). The work_category itself is resolved by `work_categories.resolve_work_category` as **explicit `--work-category` → the classification recorded in `task-manifest.json` → `feature`**, so the `task/` fallback is only reached when a task has no recorded classification at all; a run that omits the flag still inherits the namespace `requirements-discovery` classified. The base ref is the commit selected by the user's `--base-ref` during the first phase's prepare. `~/.okstra/worktrees/registry.json` (guarded by flock) globally manages task-key → path/branch mappings to prevent path and branch collisions during concurrent runs. Configured sync directories are linked from the main worktree as symlinks to provide filesystem continuity across task checkouts (the sync list can be overridden by `worktreeSyncDirs` in `project.json` or the `OKSTRA_WORKTREE_SYNC_DIRS` environment variable; an empty array disables syncing). This sync does not expand the okstra context/write boundary. Provisioning is skipped when the caller is already inside another worktree or project_root is not a Git repository, and the executor works directly from project_root. The worktree is not automatically deleted after a run; it is the authoritative artifact for later phases, PR authoring, and rollback verification. Manual cleanup: `git -C <main-worktree> worktree remove <path>` → `git -C <main-worktree> branch -D <branch>` + remove the registry entry. See the *Task worktree* block in `prompts/profiles/implementation.md` and the *Task worktree (BLOCKING for every task-type)* section in `prompts/lead/okstra-lead-contract.md` for details.
- **Isolated implementation-stage worktrees (concurrent parallelism)**: The task-key worktree above is the model for `requirements-discovery` through `implementation-planning`. `implementation` tasks use **stage isolation**: **one run = one stage**, and every run receives an isolated worktree at `.../<task-id-segment>/stage-<N>/` (branch `<work-category-namespace>/<task-id-segment>-s<N>`). The registry reserves task keys and **stage keys** (`<task-key>#stage-<N>`) together under flock. The Stage Lifecycle Snapshot reads `done`/`started` entries in `consumers.jsonl`, carry-sidecar backfills, and reserved registry stages together, and removes them from the ready set (occupancy SSOT = registry). Thus, if the user starts two `implementation` runs simultaneously, they proceed on different independent stages without collision. Base selection: independent = common anchor (HEAD fixed at entry to the first stage); single dependency = predecessor's done commit; multiple dependencies = task worktree HEAD only if every predecessor is an ancestor (`git merge-base --is-ancestor`; otherwise `PrepareError`). The cost-aware-design ready-set batch has been retired because each stage needs an isolated branch and reserving two stage keys on one branch creates a branch-uniqueness collision, so it offers no benefit: sequential work uses the next run after a stage is done, and concurrent work uses separate runs, at equivalent cost. Select a stage with `--stage <auto|N>` or the wizard's `stage_pick`. The wizard's `stage_pick` is a multiselect that labels each stage with its state (`mark_done`/`mark_active`/`mark_ready`/`mark_blocked`), topologically sorts the dependency closure of the selection with Kahn's algorithm (`stage_targets.order_stage_closure`), and exports it as the `chain-stages` CSV in render-args. The `okstra-run` SKILL consumes this queue and sequentially executes N single-stage runs in dependency order as an unattended chain, advancing only after checking the Phase 6 `done` row for each stage. This is an orchestration layer only; the **one run = one stage** isolation invariant of the wizard and prepare remains unchanged. Not only worktrees but also **run artifacts (reports, state, worker results, manifests) are isolated per stage under `runs/implementation/stage-<N>/`**, so reports and state from two concurrently running stages do not mix. In contrast, `consumers.jsonl` and the worktree registry remain at the task-type root (`runs/implementation/`) because they are shared coordination sources of truth across stages.
- **Isolation of single-stage final-verification run artifacts (concurrent parallelism)**: Single-stage `final-verification` (`--stage <N>`) also isolates run artifacts under `runs/final-verification/stage-<N>/`, like implementation, with independent sequences per stage, and appends `-fv-s<N>` to the team name. The `-fv-` delimiter prevents collisions with the same stage's implementation team (`-s<N>`) and with the default whole-task verification name. Thus, final-verification for multiple stages can run concurrently without mixing state, worker results, reports, or teams. It does not create a new worktree; it reuses the corresponding implementation stage worktree from the registry read-only and therefore does not reserve a registry stage key. The `-fv-s<N>` suffix on the `teamName` label is only for audit/display distinction. The actual team is the per-session implicit team (`session-<leadSid>`), so the pre-v2.1.178 hard failure caused by a `TeamCreate` name collision no longer occurs. Whole-task verification (empty stage value) retains the existing flat `runs/final-verification/` structure.
- **Final-verification target acquisition seam**: `stage_targets.acquire_final_verification_target()` accepts semantic task identity, the approved plan, the normalized Stage Map, and `stage: int | None`; it derives ledger, registry, worktree, Git, and integration facts behind one task-key `worktree_provision_mutex`. Single-stage acquisition is read-only and whole-task acquisition integrates and tears down completed stage worktrees. `run.py` remains the adapter that converts CLI values, builds render-context fields, computes the diff summary, and writes the target snapshot. The container keeps using the locked `resolve_and_integrate_whole_task()` interface; both interfaces share an internal unlocked implementation so the acquisition path never re-enters the non-reentrant task-key mutex.
- Every phase except `implementation` and `release-handoff` prohibits source-code edits, builds, migrations, deployments, and other state-mutating commands (`final-verification` permits read-only test commands only). `implementation` allows edits/commits only within the approved plan's file list; `git push`, publish, deploy, real migration, and third-party write APIs remain prohibited. `release-handoff` does not modify source code and executes only the commit / push / PR commands selected by the user in the menu (force push, direct push to the base branch, hook bypass, and release publication remain prohibited).
- Even if the user says something like "continue to the next step," that statement alone does not automatically begin the next phase. The next phase begins only with a new `okstra.sh` execution.
- **Authority & permissions assumption (shared by every task type and `okstra-schedule-gen`)**: Assume that the user and team have full authority and approval authority for every anticipated action. Do not include external approvals, third-party access, role/IAM permissions, organizational sign-off, legal/security review, vendor coordination, or questions about whether permission is held in routing decisions, missing inputs, clarification questions, risks, dependencies, open questions, or effort/day estimates. Internal okstra phase handoffs such as the `approved:` frontmatter in `implementation-planning` are gates the user can approve immediately and are unaffected. Forbidden `implementation` actions such as `git push`, production deployment, and shared-DB migration also remain prohibited for **safety reasons**, not permission reasons.
- Detailed phase rules are defined in `prompts/profiles/<task-type>.md`, whose body is rendered unchanged into `instruction-set/analysis-profile.md`.

### Information transfer between phases

- After the user fills answers into the `## 1. Clarification Items` section of a final report produced by `requirements-discovery`, `error-analysis`, or `implementation-planning`, carry that file into the next run with `--clarification-response <previous-final-report.md>`.
- The carried-in file is copied to the current run's `instruction-set/clarification-response.md`; the lead updates each prior `Q*` row's `Status` (`resolved` / `obsolete`) in Section 0 before proceeding.
- To edit answers and rerun in one operation, use `--resume-clarification`. See the `### --resume-clarification` section for details.
- A comparison-mode `implementation-option-selection` report exports the confirmed direction in a separate `DIRECTION SELECTION` sidecar. A new planning run consumes the selection report through `--selected-direction`, validates the sidecar and source-data digest, and writes the normalized `instruction-set/selected-direction.json` snapshot. Preselected-validation mode uses its confirmed upstream direction and needs no selection sidecar.
- **Stage carry-in (`implementation` → next stage)**: Every `implementation` run writes a `runs/implementation/carry/stage-<N>.json` evidence sidecar (flat under the task-type run dir — stage-shared, like `consumers.jsonl`). The next stage automatically carries in this file. Reverse links identifying which `implementation` run consumed each stage accumulate in the shared coordination file `runs/implementation-planning/consumers.jsonl`.

### Fix cycle (post-release bug hotfix history)

If a bug is discovered in artifacts after a task has completed release-handoff, fix it by reentering the same task ID through an entry phase (`requirements-discovery` / `error-analysis` / `implementation-option-selection` / `implementation-planning`). There is no dedicated hotfix task type, and the phase gates remain unchanged. This set of reentry runs is a **fix cycle**. Its source of truth is the append-only event rows (`opened` / `run` / `closed`) in `<task_root>/history/fix-cycles.jsonl`, owned exclusively by the module `scripts/okstra_ctl/fix_cycles.py`. The entry-phase list is defined only in `fix_cycles.FIX_CYCLE_ENTRY_PHASES` and is shared by the prepare gate and wizard detection predicate.

- **Entry**: The okstra-run wizard detects reentry into an entry phase for a completed task and confirms it at the `fix_cycle_confirm` step. The CLI uses `--fix-cycle <yes|no>` (when omitted, nothing is recorded). `--fix-cycle yes` opens a cycle only if both guards pass: task type is an entry phase, and the manifest's `workflow.lastCompletedPhase` is `release-handoff`; a violation raises `PrepareError`. A task may have only one open cycle at a time.
- **Attachment**: While a cycle is open, all runs for the same task attach to it as `run` rows even without a later `--fix-cycle` flag, and `fixCycleId` is recorded in run-manifest and timeline entries.
- **Closure**: After a release-handoff run attaches to an open cycle and the manifest's `workflow.lastCompletedPhase` becomes `release-handoff`, the next prepare lazily appends a `closed` row.
- **Consumers (all derived views)**: ① the `## Fix History` section of a later run's analysis-packet ② quotation in Task Continuity Notes from okstra-brief-gen ③ final-report `## 5.10 Fix History` (when run-manifest has `fixCycleId`, validator `_validate_fix_cycle` requires a `fixCycle` block in data.json) ④ a `fixCycles` summary in task-manifest plus a one-line entry in task-index / task-catalog. All four consumers read only derived views from `fix_cycles.summarize()` / `packet_summary()`.

### release-handoff stage-group mode

`release-handoff` operates in two modes.

- **whole-task (default)**: Exports the entire task as one PR. This is the existing behavior.
- **stage-group**: Selects some stages that received `accepted` in single-stage final-verification and combines them into one PR on a collector branch. This allows a PR to be opened for a verified stage group without waiting for the entire task to finish.

Entry is based on **task ID**, without a brief. Briefs are inputs to entry phases; for release-handoff, prepare determines eligibility from the approved plan's Stage Map + Stage Lifecycle Snapshot, then automatically creates an input document (`<task_root>/release-handoff-input.md`) that cites the verification reports. Stage selection comes from the okstra-run wizard's `handoff_stage_pick` multiselect (eligible stage groups / whole task when an accepted whole-task report exists) or CLI `--stages <csv>`, and is exposed to the lead as `HANDOFF_MODE` / `HANDOFF_STAGES` in run-context.

The stage-group interaction order is: **G1 select base → G2 confirm stages (selection finishes before prepare—do not ask again) → assemble (create collector branch + merge selected stages) → conflict probe → draft PR → push/PR**.

- The Stage Lifecycle Snapshot is the source of truth for eligibility. The Snapshot reads `verified` entries (which stages were accepted in single-stage final-verification), `pr` entries (which stages went into which PR), and done rows in `consumers.jsonl`, then computes stages that are `verified` but not yet included in a `pr` as candidates.
- The worktree registry reserves stage-group occupancy under the key `<task-key>#group-<id>`, and collector branches are named `<work-category-namespace>/<task-id-segment>-g2-3` (for example, `-g2-3` when stages 2 and 3 are selected).
- Enforcement is implemented in the Python module `okstra_ctl.handoff`, not merely declared (`okstra handoff <subcommand>`). It has four subcommands: `eligible` (query eligible stages) / `assemble` (create and merge the collector branch) / `record-verified` (record a `verified` row) / `record-pr` (record a `pr` row). Merging during collection is an explicit exception to the isolation spec's non-goal that "okstra does not merge automatically."

### improvement-discovery (sidetrack entry-point)

````
[brief: scope=codebase + priority-lenses]
        ↓ okstra-run --task-type improvement-discovery
[improvement-discovery]
        ↓ final-report (## 5.9 Improvement Candidates, N candidates)
        ↓ (user selects K candidates and writes a new brief for each)
[requirements-discovery | implementation-option-selection | error-analysis] (new task-id per selected candidate)
````

A sidetrack entry point that is not a formal member of `PHASE_SEQUENCE`. It supports codebase-discovery scenarios without breaking the one-way lifecycle. The lens allowlist and candidate cap are consolidated in the single source of truth `scripts/okstra_ctl/improvement_lenses.py`. `validators/validate_improvement_report.py` checks eleven contract items against the final report; one of them is the shape of the `## 5.9 Improvement Candidates` table, whose eleven columns run from `Cand ID` through `Evidence` (the two counts are independent and happen to coincide). Two bidirectional grilling points—an enhanced budget of 8 in `okstra-brief-gen` Step 4 and the lead's Phase 1.5 reflect-back budget of 12—align the user's and AI's understanding.

### Read-only analysis sidetracks

`project-analysis`, `feature-analysis`, and `change-impact-analysis` are independent entry points outside `PHASE_SEQUENCE`. They share the normal task identity and report pipeline but never route into one another automatically. All three are read-only with respect to the target project: workers may inspect the confirmed scope but may not edit files, run tests or builds, execute migrations, or deploy.

`analysis_inputs.py` is the shared resolution boundary for both the wizard and `prepare_task_bundle()` (`scripts/okstra_ctl/analysis_inputs.py`). It owns the three-type allowlist, the permitted evidence relationships, report identity checks, review eligibility, source-commit freshness, and `PF-NNN` feature-index target resolution. Prepare freezes the result in `run-manifest.evidenceInputs` and `run-manifest.analysisTarget`; `validators/validate_analysis_report.py` requires the report's `analysisCommon.evidenceInputs` and resolved scope to match those snapshots exactly. The stored freshness value is `exact` when the evidence source commit is current and `stale` otherwise.

The self-contained report view presents an `Analysis Review` control. The browser's Export action downloads the Markdown sidecar; it cannot write into the project filesystem. The user must save or move that download into the canonical `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` location before a later run can consume it. The downloaded sidecar contains a block named `ANALYSIS REVIEW` (Markdown heading `## ANALYSIS REVIEW`) without mutating the original report. It records `accepted`, `revision-requested`, or `rejected`; only accepted reports are offered automatically as later evidence. An explicitly selected unreviewed report is marked `user-unverified`, while revision-requested and rejected reports are refused as evidence.

A revision request supplies affected report IDs and a reason. The wizard prioritizes the same task type on the same task, and carries that report into a full rerun. The rerun reanalyzes the whole confirmed scope rather than only the disputed rows, then writes `analysisReviewResolution` under `analysisCommon` for every affected ID. `validators/validate_analysis_report.py` checks the source report identity, task type, run sequence, exact affected-ID coverage, and each resolution outcome. Report verdict (`analysis-complete`, `analysis-partial`, or `blocked`), review status, evidence freshness, and direct user verification (`user-unverified`) remain separate axes.

### requirements-discovery fan-out

For mixed or multi-item requests, requirements-discovery splits the request into packets by domain (the five-value work-category enum) and publishes them to `runs/requirements-discovery/fan-out/unit-*.md`. Each packet becomes a new task key through `okstra-run --task-brief <path>`. The dependency topological order is recorded in `index.md`, and okstra-schedule-gen owns the integrated schedule after task creation. okstra-brief-gen is not involved in this path. Validation: `validators/validate_fanout.py` (validate-run hook).

### Worktree preview at the confirm step

The okstra-run wizard does not add a separate branch-confirmation step. Instead, the final summary block (`confirmation_block`) immediately before `confirm` shows one `worktree` line indicating "create a new branch/worktree vs reuse the current worktree." The decision is previewed through the side-effect-free `worktree.preview_worktree_decision()`, and `provision_task_worktree` uses the same helper during execution, so preview and reality match. Because `implementation` uses stage isolation, the preview reflects the stage worktree the current run will actually use rather than the task-key directory (`resolve_stage_worktree_decision`; stage `auto` uses `compute_worktree_path`). `provision_stage_worktree` re-runs that decision at execution time. `final-verification` omits the worktree line because it reuses a stage worktree read-only. `confirm` offers three options: `Proceed`/`Edit`/`Abort`. `Edit` rewinds to any step, including base-ref selection. `Abort` is terminal: state is fixed as `aborted`, subsequent `next_prompt` calls return only `kind: "aborted"`, and `render-args` rejects the request with `ok: false`.


---

## Storage model & contracts

The complete specification for `okstra`'s three storage areas (stable task root / per-run artifacts / `~/.okstra` indexes), along with the task manifest, task index, run manifest, timeline, and Claude operating contract, has moved to a separate document: [`architecture/storage-model.md`](architecture/storage-model.md).

## Task brief usage

`okstra` is brief-first. The brief is the canonical source material that preserves external input and okstra augmentations. Any additional material workers need—reports, code snippets, logs, and so on—must be included inline or by path in the brief's `Evidence and Source Materials` section.

Briefs are accepted as direct input **only for entry phases**: users provide a brief path for `requirements-discovery`, `error-analysis`, `improvement-discovery`, `project-analysis`, `feature-analysis`, and `change-impact-analysis`. Downstream phases (`implementation-option-selection` / `implementation-planning` / `implementation` / `final-verification`) automatically carry in the task manifest's `taskBriefPath`; a new planning run additionally requires its selected-direction report. The okstra-run wizard does not ask for the brief again. If the brief is unregistered, a fallback picker recommends switching to an entry phase. `release-handoff` has no brief; prepare generates an input document that cites verification reports.

A brief is a **translation layer**: it converts external input—an issue-tracker ticket, requirements document, or user message—into an okstra-readable format while preserving the original verbatim and clearly distinguishing okstra additions as labelled augmentation. The output of the `okstra-brief-gen` skill is the source of truth. For each analysis phase, `prepare_task_bundle()` extracts the necessary frontmatter, task-specific brief sections, reference expectations, carried-in clarification, and directive into `instruction-set/analysis-packet.md`. This compact packet is the analysis workers' primary input; the original brief and profile/material files are fallback evidence opened only to verify evidence or fill omissions.

A brief typically includes:

- Problem description
- Requirements
- Existing analysis
- Raw sample or log
- Related code paths
- Constraints
- Questions for workers
- Expected output
- Previous-run or related-task information
- (Optional) glossary candidates—okstra-brief-gen Step 4.5 writes these directly to `<PROJECT_ROOT>/.okstra/glossary.md`
- (Optional) decision candidates—evaluated in the `implementation-planning` phase and promoted to `.okstra/decisions/<NNNN>-<slug>.md`

Step 6.5 of okstra-brief-gen is **reporter batch confirmation**. It asks the user to confirm, in one batch, whether meaning changed while external input was translated into the brief, and records the results in the `Reporter Confirmations` section. Every analysis profile requires this section as a precondition to phase analysis (validator: `validators/validate-brief.py`).

Default templates:

- `templates/reports/quick-input.template.md`
- `templates/reports/task-brief.template.md`
- `templates/reports/error-analysis-input.template.md`
- `templates/reports/project-analysis-input.template.md`
- `templates/reports/feature-analysis-input.template.md`
- `templates/reports/change-impact-analysis-input.template.md`
- `templates/reports/implementation-planning-input.template.md`
- `templates/reports/implementation-input.template.md`
- `templates/reports/final-verification-input.template.md`

At minimum, fill the following fields in input templates:

- `Project ID`
- `Task Group`
- `Task ID`
- `Related Tasks`
- `Task Type`
- `Requested Outcome`

## Recommended workflow

### 1. Write a draft

Use `okstra-quick-input.template.md` to organize notes quickly.

### 2. Write a formal brief

Organize the final input as `okstra-task-brief.md`.
If you plan to continue the same task later, keep the same `task-group` and `task-id`.

### 2.5. Triage requirements if needed

If you first need to classify whether the request is a bug fix or new feature, or determine whether the requirements are sufficient, set `Task Type: requirements-discovery` in the same `okstra-task-brief.md` and run it.
This stage does not automatically execute later phases. It only records the work category and next recommended phase in task lifecycle metadata.

```bash
scripts/okstra.sh --task-type requirements-discovery --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path>
```

### 3. Render-only validation

```bash
scripts/okstra.sh --render-only --task-type error-analysis --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path>
```

### 4. Run Claude

There are two entry methods, and they produce identical artifacts.

**Option A — launch a new Claude process (`okstra.sh`)**

```bash
scripts/okstra.sh --task-type error-analysis --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path>
```

On this standalone compatibility path, `okstra.sh` finishes the prepare stage and then runs a new interactive Claude session via `exec` from the target project root. It performs only the handoff; the session continues under the Okstra lead contract. Because `sessions/claude-resume-<task-type>-<seq>.sh` is written immediately before execution, the same run can be resumed if interrupted. In-host Claude Code, Codex, and Antigravity paths retain the current native session instead.

**Option B — hand off within the current host session (`okstra-run` skill)**

When using an installed host adapter, the current native session assumes the Okstra lead role without launching a replacement lead process. Example triggers: `"run okstra here"`, `"start error-analysis on this project"`.

Skill flow:

1. The `okstra-run` skill activates and relays task candidates, task type, and brief path through the current host's question or text interface.
2. It calls `okstra_ctl.run.prepare_task_bundle(render_only=True)` with the user's input—directly invoking the same Python function without passing through `okstra.sh`.
3. After the same instruction-set artifacts are written to disk, the current host reads the canonical lead prompt and assumes the lead role.

### Agent invocation contract

An agent identity is functional, not provider-specific. Each LLM invocation is
composed from three independent inputs: the selected model assignment, a duty
contract from `prompts/duties/`, and call-specific task instructions. The
composer publishes the final prompt and adjacent metadata only after the
complete run manifest and its immutable `agentContract` and
`invocationAssignments` maps are on disk. Later manifest rewrites reject any
change to those fields or to the canonical lead-prompt path. This prevents a
prompt from being verified against state that appeared only after dispatch.

A duty is bound to the **role** an invocation performs, not to its phase. The
analysing phases therefore share four contracts rather than one: `discovery-worker`
(requirements-discovery, improvement-discovery — hand over candidates without
starting them), `diagnosis-worker` (error-analysis — fix the symptom, establish
reproduction, submit only falsifiable causes), `planning-worker`
(implementation-planning — realize one selected direction, stage the work, and never approve its
own plan), `direction-selection-worker` (implementation-option-selection — compare or validate directions without writing a detailed plan), and `analysis-worker` for the observational phases (project-,
feature-, and change-impact-analysis), which describe an area without designing
for it. The map lives in [`scripts/okstra_ctl/worker_prompt_policy.py`](../scripts/okstra_ctl/worker_prompt_policy.py)
`ANALYSIS_DUTY_BY_TASK_TYPE`; an unmapped analysis task type takes the
observational default. `run.py` resolves the manifest's `allowedAudiences` from
that same map, so a phase can only be dispatched with the duty its role owns.

`invocationAssignments` is the assignment source of truth for the lead,
initial workers, critics, translators, reverification workers, and report
writer. Each `assignmentRef` resolves to exactly six fields: `provider`,
`model`, `modelExecutionValue`, `runner`, `hostRuntime`, and `hostModelValue`.
Invocation metadata repeats that resolved value under `modelAssignment`; the
verifier requires byte-equivalent canonical data rather than resolving the
model again. Initial and dynamic calls also reserve `invocationId`, `workerId`,
`assignmentRef`, audience, dispatch kind, prompt path, and metadata path under
the run's invocation-reservation root. Reusing an invocation ID with a
different identity fails as a conflict.

The adjacent metadata has one digest location, `digests`, containing exactly
`catalogDigest`, `assignmentDigest`, `dutyDigest`, `instructionDigest`, and
`promptDigest`. Assignment and instruction values use UTF-8 JSON with sorted
keys, compact separators, and no non-finite numbers before SHA-256 hashing.
Catalog and duty hashes use a versioned frame of sorted relative file names and
raw bytes. The prompt hash covers the final UTF-8 bytes. Instruction provenance
uses only `{kind: project|runtime, path: <relative POSIX path>}` entries under
`instruction.sourcePaths`; installed runtime absolute paths are never persisted.

Prompt publication uses an exclusive per-invocation lock and create-only file
publication. The prompt is published first and its metadata is the completion
marker published last. A retry may recover a matching prompt that has no
metadata; any differing prompt or metadata is an immutable conflict. The
reservation root has its own lock, so concurrent writers either converge on
the same specification or one fails without mixing artifacts from two writers.

`modelExecutionValue` is the provider-process value. `hostModelValue` is the
model argument understood by a host-native invocation primitive. They are
separate because a host may use a family token while the provider CLI requires
a concrete execution identifier. A code-owned provider launch goes through
`okstra worker-dispatch`, verifies the invocation immediately before process
creation, starts all selected analysis assignments before collecting any one
of them, and records `core-pre-dispatch`. Report writer is a Phase 6 dependency
and must run in its own explicit dispatch after convergence; the dispatcher
excludes it from default selection and rejects any mixed analysis/report batch.
Each completed result path must link to
exactly one dispatch, and one dispatch cannot authorize multiple accepted
results. A host-native launch records
`host-native-spec-link-gate` and links its accepted result through
`okstra agent-prompt link-result`; this proves association with the verified
specification, not observation of the prompt bytes delivered by the host.
Accordingly, host-native records retain `promptDeliveryVerified=false` unless
a future host supplies a verifiable receipt or transcript. The process-owned
lead launch performs the same pre-dispatch verification and records its lead
dispatch before process creation; an in-session lead can only apply the weaker
specification-link gate because Okstra cannot intercept the host primitive.

The same boundary covers the lead, initial and dynamic workers, critics,
report writing, translation, manager child leads, and the standalone code-review
and schedule-verification agents. `okstra codex-dispatch` is retained only as a
compatibility alias. Provider-specific LLM transport agents are not installed.
The implementation executor manifest has no `workerAgent` field: it records the
provider, both model values, runner, and dispatch mode, while `doctor` checks
either the native host adapter or deterministic dispatcher required by that
actual execution mode.

See [`skills/okstra-run/SKILL.md`](../skills/okstra-run/SKILL.md) for the detailed procedure.

### 5. Analyze errors if needed

```bash
scripts/okstra.sh --task-type error-analysis --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path>
```

After the first entry, use the abbreviated form for additional runs of the same task:

```bash
scripts/okstra.sh --task-key <project-id>:<task-group>:<task-id> --task-type error-analysis
```

### 5.5. Rerun immediately after answering

If you need to fill answers into Section 5 of a `requirements-discovery` or `error-analysis` final report, handle the edit and rerun in one operation rather than carrying the path manually.

```bash
scripts/okstra.sh --resume-clarification --task-key <project-id>:<task-group>:<task-id>
```

The latest final report opens in `$EDITOR`, and after saving, the same phase reruns automatically with the report carried in through `--clarification-response`.

#### Incremental re-verification (implementation-planning reruns only)

The default for an `implementation-planning` clarification rerun is **full re-verification**. However, if an answer has only local impact and the code is unchanged, the lead re-verifies only the downstream closure of affected stages and carries forward the other stages' plan-item verdicts from the previous run. The decision is split between deterministic CLI logic and lead judgment.

- **C1 (CLI decision, deterministic)**: The `executorWorktree.baseRef` in the previous run's `state/active-run-context-implementation-planning-<prev-seq>.json` must match the current run's base-ref SHA. If they differ, the code changed, so the result is immediately `full`.
- **C2 (lead judgment)**: The set of Stage Map stage numbers affected by the answer. Never include a number not in the Stage Map. An answered `C-NNN` that traces to no stage is `mode: unresolved`, not full — ask the user for those stage numbers (`--impacted`) rather than widening the whole re-run. If the answer reverses the selected Option, Stage Map, or approach, pass `--full-reason`; that is the only structural path that still forces `full`.
- **Closure cutoff**: `okstra incremental-scope` calculates the `downstream_stage_closure` of affected stages in the `implementationPlanning.stageMap` dependency graph. If its size is **more than half** of all stages, the result is `full`; otherwise it outputs `{mode, reverify_stages, carry_stages, reason}` JSON. When the wizard pin is `auto` and the CLI returns `mode == "incremental"`, the lead must not upgrade to full.

```bash
okstra incremental-scope --prev-data <prev data.json> --cur-base-sha <sha> --prev-base-sha <sha> --impacted 2,3
okstra incremental-carry --prev-data <prev data.json> --cur-narrative <cur narrative.md> --state <cur plan state.json> --prev-seq <prev-seq> --carry-stages <csv> --reverify-stages <csv> --out-state <cur plan state.json>
```

When `mode == "incremental"`, worker dispatch is narrowed to `reverify_stages` (see *Cross-verification mode* in `prompts/profiles/implementation-planning.md`), and workers do not reopen or rejudge `carry_stages`. The report writer preserves carried stage rows in its narrative. After plan-item seeding, `okstra incremental-carry` verifies those rows and copies their previous `P-Step-*` and `P-Prep-*` verdicts, plus unchanged `P-Val-*` / `P-Req-*` / `P-Rb-*` rows, into the convergence-owned state with a `carriedForwardFromSeq` tag. It exits nonzero on a changed or omitted carried stage, missing item, or conflicting scope and falls back to full. If a reverified stage concludes that a plan item must be **deleted**, that indicates nonlocal impact and likewise triggers a full rerun. `verdictCard` / `finalVerdict` are never carried and are recomputed on every run.

The report writer records the decision unchanged in its narrative at `implementationPlanning.incrementalDecision`. Report assembly carries the field into `data.json`, and the renderer exposes it as a `### 0.1 Incremental Re-Verification Scope` audit block. The source procedure is the §"Incremental re-verification" section of `prompts/launch.template.md`.

### 6. Review the implementation plan if needed

```bash
scripts/okstra.sh --task-type implementation-planning --workers claude,codex --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path>
```

Abbreviated form for a later phase (if the manifest's `workflow.nextRecommendedPhase` is `{"phase": "implementation-planning", "status": "ready", ...}`, `--task-type` may also be omitted):

```bash
scripts/okstra.sh --task-key <project-id>:<task-group>:<task-id> --workers claude,codex
```

### 7. Implement from the approved plan

Run only when the report record `frontmatter.approved` field in the `implementation-planning` final report is `true`. The approved plan path must be carried in through `--approved-plan`; if the plan is missing or the record is not `approved: true`, the run is rejected as `contract-violated`.

Approval format (code truth: `_record_approved_flag` / `_validate_approved_plan` in `scripts/okstra_ctl/run.py`):

- The report record owns `frontmatter.approved` as a boolean. The full reading copy may display the same value; the gate does not read that display.
- Schema-v1 reports have no record sibling; their markdown YAML `approved: true|false` line is the record (`APPROVED_FRONTMATTER_PATTERN`).
- The report writer always publishes `approved: false`; implementation becomes available after the user approves it.

To treat the CLI invocation itself as approval, add the `--approve` flag. okstra sets the record to `true`, refreshes the full reading copy, and continues into the implementation phase (the former `--ack-approved` alias was removed in 0.8.0). See [`docs/cli.md`](cli.md#--approve) for details. Editing the full reading copy does not approve the plan.

```bash
scripts/okstra.sh --task-type implementation --workers claude,codex,antigravity --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path> --approved-plan <runs/implementation-planning/.../reports/final-report.md>
```

Abbreviated form:

```bash
scripts/okstra.sh --task-key <project-id>:<task-group>:<task-id> --task-type implementation --workers claude,codex,antigravity --approved-plan <runs/implementation-planning/.../reports/final-report.md>
```

### 8. Run the final review after implementation

```bash
scripts/okstra.sh --task-type final-verification --project-id <project-id> --task-group <task-group> --task-id <task-id> --task-brief <brief-path>
```

Abbreviated form:

```bash
scripts/okstra.sh --task-key <project-id>:<task-group>:<task-id> --task-type final-verification
```

### 9. Resume the same task

When reopening the same bugfix or workstream, use the same `task-group` and `task-id`.
History then continues accumulating in `runs/` and `history/timeline.json` under the existing task root.
After the first entry, the abbreviated `--task-key <p>:<g>:<i>` form is the most concise; missing brief/task-type values are populated automatically from the manifest.

## Lifecycle status and resume

For long-running work, the `workflow` object in `task-manifest.json` is the canonical lifecycle state.
Checking the following fields first makes it easy to determine the current position and resume point:

- `workflow.currentPhase`
- `workflow.currentPhaseState`
- `workflow.phaseStates`
- `workflow.lastCompletedPhase`
- `workflow.nextRecommendedPhase` *(object `{phase, status, rationale}`; `status` is `ready` / `pending` / `blocked` / `terminal`. The lead writes `phase` only under `ready` — an authoring rule, not a constraint the struct enforces: `prepare` lowers a `ready` pointer to `pending` and keeps its `phase`, so a non-`ready` pointer that still names a phase is a normal manifest state. Read `status` to decide whether anything can be launched.)*
- `workflow.nextRecommendedPhaseCorrection` *(present only when Phase 7 validation replaced the authored pointer — `{authored, applied}`, where `authored` is the pointer the lead wrote)*
- `workflow.awaitingApproval`
- `workflow.lastSafeCheckpoint`
- `phaseOutcome`

Use `.okstra/discovery/task-catalog.json` to review project-wide state.
For the latest state of a specific task, check `.okstra/discovery/latest-task.json`, `task-manifest.json`, the latest `run-manifest`, and `history/timeline.json`, in that order.

In Claude, use the `status` subflow of the seeded `okstra-inspect` skill to answer questions such as:

- Show all okstra task statuses
- Show the current phase and next phase for a specific `task-key`
- Show tasks awaiting approval and tasks that can be resumed
- Update `<task-id>`'s `workStatus` to `todo` / `in-progress` / `blocked` / `done` (for example, "okstra mark <task-id> done", "mark <task-id> in progress")—the skill updates `workStatus`, `workStatusUpdatedAt`, and `workStatusNote` in the corresponding `task-manifest.json`.

Resume decision rules:

- Resume the current run: if `latestResumeCommandPath` exists, use that path first.
- Await user approval: if `workflow.awaitingApproval` is true, ask the user to approve the plan (`okstra-run` implementation wizard, or `--approve`). Do not start implementation as if the gate were already cleared, and do not re-run `implementation-planning`.
- Restart the current phase: run again with the same `task-key` and current `task-type` only when `awaitingApproval` is false and the pointer is not `ready`.
- Start the next phase: if `workflow.nextRecommendedPhase.status` is `ready` and `awaitingApproval` is false, prepare the next `okstra.sh` run with its `phase`.
- Additional material required: if that `status` is `pending` or `blocked`, the pointer carries no phase. Read its `rationale`. After planning, `blocked` means answer the named `C-NNN` rows with `okstra-user-response` first; do not re-run planning until those answers exist.
- Nothing left to run: `status` `terminal` means the lifecycle ended at this phase.

## Final report structure

New task bundles use `schemas/final-report-v3.0.schema.json` as the final-report data contract. The report writer authors the `humanSummary` and exactly one task-type deliverable in narrative Markdown. Report assembly adds the single-owner machine inputs and atomically publishes one data.json source of truth. Two renderers consume it independently. Historical `schemas/final-report-v2.0.schema.json` records remain readable.

- `templates/reports/final-report-v2.template.md` produces the full reading copy Markdown spine in a fixed order: handoff summary, decision context, next-task contract, clarifications, evidence ledger, one task deliverable, cross-verification audit, execution audit, and token/cost audit. The task deliverable body comes from `templates/reports/md/tasks/<task-type>.template.md`, the Markdown sibling of the HTML task template below. Human narrative fields are excluded from this artifact.
  - Section bodies are rendered as Markdown — headings, tables for uniform row sets, prose for narrative fields — by `scripts/okstra_ctl/report_markdown.py`, never serialized as JSON. Field order within a section comes from the record's versioned schema, whose properties are authored in reading order (a decision draft reads `context → decision → consequences`).
  - Each task template names the sections a reading agent should meet first and closes with a `md_rest()` sweep, so a field added to the schema reaches the Markdown without a template edit. **Enforced:** `tests/contract/test_ai_markdown_rendering.py`.
- `templates/reports/html/tasks/<task-type>.template.html` produces the task-specific human-facing HTML. It leads with a plain-language decision summary and presents the selected task's findings, diagrams, tables, evidence, and actions. Worker execution and convergence detail stays in a visually subordinate audit section.

The `## 0. Reading Confirmation` block from worker output is written to the sidecar at the resolved `<run-dir>/worker-results/<worker>-audit-<task-type>-<seq>.md`, not included in the report body (enforced by the validator).

If there are no substantive differences, state that fact rather than manufacturing a contrast.
Write the actual Markdown report body to the file instead of metadata about save failures or session limitations.

### Implementation-planning activity and approval contract

The manifest-provided `lead-events-*.jsonl` file is the canonical record for structured agent activity. Activity writers append `eventType: "activity"` rows to that file and receive monotonically increasing `A-NNN` identifiers from the shared append path.

`activityContractVersion: 1` is an interpretation version for new `implementation-planning` artifacts. A run without that field remains a historical run and does not require activity events or an `agentActivity[]` projection.

The shared `okstra report-finalize` entrypoint reads canonical activity before translation source checking. Under contract v3 its in-process `project-activity` step assembles the report, filtering events by run identity and validating activity ID order before `agentActivity[]` is published. A schema-invalid composed record is still published so `validate-run` can scan it; the step itself fails. A historical v2 manifest retains the in-place projection path. A failed Phase 7 step does not skip later checks: `validate-run` still runs, and only `teardown-stages` is skipped. For a non-English report, the lead runs `token-usage`, `project-activity`, and `check-source` before translator dispatch, then resumes at `render-views` after the translation sidecar exists. Conformance compares the resulting `agentActivity[]` IDs, order, and core fields with the canonical events for every lead host.

Approval blockers use `open`, `answered`, `resolved`, and `obsolete`. `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block approval or the next phase; the DISAGREE votes stay on the plan item as evidence. A response sidecar with a proceeding disposition unblocks the same way. `request-revision` and `reject` still withhold the next phase until this report's `supersessionLedger` records that the answer was incorporated (`superseded` or `no-dependent-statement`).

An approval row classifies its cause as `user-decision`, `noncritical-dissent`, or `correctness-critical`. `accept-risk` is available to all three: it ends the gate and keeps the dissent on the record for later stages. `correctness-critical` can still take `request-revision` when the user wants the plan corrected and re-verified.

## Final report views (HTML)

The Phase 7 `render-views` step accepts either a final-report data.json or its Markdown sibling. For schema v2 and v3, it locates and validates `final-report-<task-type>-<seq>.data.json`, selects the task type fail-closed, and renders HTML directly from the structured data. It does not parse the report-writer narrative back into a human model. The lead reaches this step through `okstra report-finalize`, which owns the shared Phase 7 sequence in `scripts/okstra_ctl/report_finalize.py`.

- `reports/final-report-<task-type>-<seq>.html` — always generated for schema v2 with one of eleven dedicated task templates. It includes an accessible summary, task-specific prose, tables and inline SVG diagrams, evidence references, decisions, and next actions. CSS / JS are embedded inline with no external assets; print and no-JavaScript fallback content preserve the essential information.
- **Human summary**: `humanSummary` is the sole v2 top-level human summary contract. It is not copied into AI Markdown. Each task view decides how to present it together with the task deliverable instead of sharing a generic dashboard body.
- **Audit isolation**: worker execution, convergence, and token/cost material remain available for traceability but are subordinate to the user's findings and decisions. They never replace the task analysis narrative.
- **Implementation-planning activity**: activity-contract reports show each agent's task, summary, and outcome in the default view. Commands, exit codes, file-and-line evidence, and result paths remain inside expandable detail. Approval decision cards link to the relevant `id-A-NNN` activity anchors and preserve each option's disposition in the exported user response.
- **Schema v1 compatibility**: existing v1 data and quick Markdown reports keep the legacy conditional renderer, including `readerSummary`, reader modes, and the original Markdown-parsing path.
- **`C-*` select option order (schema v1)**: the legacy renderer parses `Expected form`, puts the `Recommended:` answer **first**, and relabels the `Alternatives:` items consecutively as `(a)`, `(b)`, and so on (the original character labels are not retained). Schema v2 parses nothing here — a `Kind=decision` row carries `options[]`, and the presentation order is the array order with the `role: recommended` entry first.

Entry points:

- Python routing references: `scripts/okstra_ctl/report_views.py` owns the version split; `scripts/okstra_ctl/report_html/router.py` maps all v2 task types to dedicated view-model builders and templates. The HTML JavaScript response serializer remains byte-identical to Python `serialize_user_response`.
- CLI: `scripts/okstra-render-report-views.py <final-report.data.json|final-report.md>` or delegated Node wrapper `bin/okstra render-views <path>`.
- Validation: `validators/validate-report-views.py` checks schema/source/template digests, required human fields, form controls, external assets, SVG/table ID parity, and Response ID parity. The v2 renderer rejects an unknown task type instead of falling back to a generic page.
- User-response sidecar schema source of truth: `templates/reports/user-response.template.md`. The in-session skill reads `okstra user-response show-view`, then the cited `path:line` artifacts and linked plan items that view lists, before asking. The recorded answer still comes from `options[]` via `--option-number`. Task pick, each clarification, and the record confirmation use the current host's native question tool from the relay `native-single` mapping (`AskUserQuestion`, `ask_user_question`, or `request_user_input`). The picker shows those choices only — it does not append `Enter directly`; custom text uses the host free-text row. A numbered list is only the fallback when that tool is missing or the option count misses `nativeLimits`.
- In-run user confirmation before an approval blocker follows the same briefing order and the same host picker: the lead investigates cited plan items, worker findings, and files, then asks through the adapter `prompt_user` mapping (`prompts/lead/okstra-lead-contract.md`).

Generating a view never modifies the original final-report MD.

## Worker error collection (optional sidecar)

Errors that occur while provider workers, the report writer, or the Okstra lead run can be collected into a single chronological log for later retrospectives.

- Storage location: resolved `<run-dir>/logs/errors-<task-type>-<seq>.jsonl` (`<run-dir>` includes `stage-<N>/` for a stage-isolated run)
- Append-only JSON Lines, isolated per run, so there is no separate rotation policy.
- **Single writer**: To avoid concurrent-append collisions, only the active Okstra lead writes this file directly. `<seq>` is an independent three-digit zero-padded counter (`001`, `002`, …) scanned per category directory (`logs/`, `manifests/`, `state/`, and so on), so category values may differ within one run when a prior run recorded only some categories. The cross-category identifier for one run is the manifest's `runDateTimeSegment` ISO timestamp field.
  - Internal worker tool failures are reported in the worker-result manifest's `errors[]`, then dumped by the lead immediately after merge
  - Codex/Antigravity CLI failures, timeouts, and rate limits are observed directly by the lead through the wrapper
  - resultContract violations, schema mismatches, and missing required fields are observed directly during lead validation
- The `source` field (`worker-reported` | `lead-observed`) distinguishes the origin, and `errorType` (`tool-failure` | `cli-failure` | `contract-violation`) distinguishes the type.
- `stderrExcerpt` is capped at 2 KB for one-line jsonl readability and protected by the `PIPE_BUF` (4096B) atomic-append guard.
- The `errorsLogPath` field in run-manifest or team-state records `logs/errors-<task-type>-<seq>.jsonl` once, relative to the resolved run directory, so it can be discovered.
- Path-delivery wiring (`f6f9f69`): `scripts/okstra_ctl/paths.py` exports `RUN_ERRORS_LOG_FILE` / `RUN_ERRORS_LOG_RELATIVE_PATH` and per-worker sidecar paths, while `scripts/okstra_ctl/render.py` exposes the `{{RUN_ERRORS_LOG_PATH}}` / `{{<WORKER>_ERRORS_SIDECAR_PATH}}` template tokens. The `## Run Logs (error-log wiring)` section of `prompts/launch.template.md` passes the resolved absolute path to the lead, which must forward it in the `**Errors log path:**` / `**Errors sidecar path:**` lines of the worker dispatch prompt. Path delivery became a formal contract after a regression in which a worker with only the literal `<runDir>/logs/...` template fragment dropped an entry through argparse exit.
- Helper CLI: `scripts/okstra-error-log.py`
  - Appends records through a single entry point.
  - Both worker-sidecar dumps (`append_observed`, guarded by schema version) and lead observations (`lead-observed`) use the same helper.

### Live log and worker-pane presentation (every CLI worker)

- On every dispatch, `scripts/okstra_ctl/worker_runner.py` creates a `<prompt>.log` sidecar next to the prompt path and writes the CLI's output there, truncating the file on each dispatch. All five entrypoints (`okstra-{claude,codex,antigravity,grok,kimi}-exec.sh`) are four-line shells that `exec` `scripts/okstra-provider-exec.py`, so the log contract is one implementation rather than one per provider. The runner reads the child's pipes directly with a `selectors` loop — there is no FIFO and no `tee` subprocess — and the child is spawned with `start_new_session=True`, which is what keeps it addressable as a process *group* for the idle watchdog's `killpg`.
- How the two streams are split depends on what the CLI speaks, and the decision lives in `_stderr_target`. A `stream-json` provider puts events on stdout and only its own error text on stderr, so the two are folded into one reader. A `text` provider (codex) splits meaning across them — the result on stdout, progress on stderr — and merging them would destroy the only way to tell the answer from the noise, so they stay apart.
- Idle is measured from **stream arrival**, never from the log file's mtime. The screen deliberately drops thinking events, so an mtime-based watchdog would SIGTERM a healthy worker in the middle of a long reasoning stretch. On a breach the runner `killpg`s the group (SIGTERM, then SIGKILL after a 5s grace), returns exit code 124, and marks the status sidecar `{timeout: true, idle_seconds, idle_at_ts, terminated_by: "idle-watchdog"}`.
- The `.log` is an execution transcript containing wrapper/CLI output, not the original prompt. `okstra log-report` pairs it with the sibling persisted `.md` prompt and reports `transcriptBytes`, `promptBytes`, and their ratio separately. The compatibility fields `path`, `sizeBytes`, and `totalBytes` continue to mean transcript data, so existing consumers and transcript-size ordering remain stable.
- **Run-wide cap on the progress copy in the log** (`_LOG_PROGRESS_LINE_CAP` in `scripts/okstra_ctl/worker_runner.py`): workers read their required inputs end-to-end per the Worker Preamble's *Reading rules*, so a single report read can dump 170KB+ into the log and observed sidecars reach 8MB. The runner archives the first 5000 progress lines of the run and replaces the rest with a `[okstra log-cap] N progress line(s) elided` marker, emitted every 500 elided lines so a reader tailing the log can see the run is still producing. The cap is run-wide rather than per-block because a block boundary is a provider's own vocabulary and the shared runner has none; the cost is that a very long run keeps its opening rather than a sample throughout, which the elision notices make visible. **Only the log copy is capped** — never the screen, and never the result stream, because a truncated tool echo costs detail while a truncated answer costs the whole post-mortem.
- **Progress lands in the worker's own pane, not in a sibling tail pane.** The presentation is passed to the entrypoint as `--presentation live|quiet`; `live` is claimed only by a backend that opened a pane, and the default is `quiet`. Under `live` the runner renders each event into one readable row on the caller's own streams (`→ Bash: …` for a tool call, `  ← ok (N bytes)` for its result, `!! PERMISSION DENIED — <tool>: <reason>` for a refusal); thinking events are dropped. Under `quiet` progress is withheld and only the worker's closing text is printed, which is what a `cli-wrapper` dispatch on a machine with no pane surface needs. `scripts/okstra_ctl/domain/worker_stream.py` owns all three projections (`format_live`, `format_log`, `final_text`) as pure functions over *normalised* events — `Text`, `ToolCall`, `ToolResult`, `Denial`, `Result`. Providers do not share a wire format and this layer may not name one, so each adapter supplies the function that turns its own events into those, declared on `ExecCommand.normalise` beside the stream format it belongs to. Adding a provider whose stream is shaped differently is that one function; a provider that declares `stream-json` without one is failed by `tests/contract/test_provider_execution_contract.py`, which runs the schema each provider claims through its own normaliser and requires rows out.
- **No okstra pane carries a tag any more, and the tag scan is gone with them.** Two writers disappeared at different times. The wrappers stopped splitting a `tail -F` sibling, so **nothing spawns a trace pane** and `@okstra_trace_run` / `@okstra_status` lost their writer — the operator watches the worker pane itself instead. Then the `tmux-pane` dispatch backend was removed, so `tmux.tag_pane` and `@okstra_worker_run` went with it: a worker now gets a cmux surface or a cli-wrapper subprocess, neither of which is a tmux pane okstra owns. `okstra-trace-cleanup.sh` therefore no longer scans tags at all, and the two modes that existed only for that scan were removed — `--reclaim-completed` (with the `okstra-subagent-reclaim.sh` entry that drove it from `SubagentStop` / `TaskCompleted`) and `--reap` (with its `SessionEnd` entry). Those hooks are gone from `templates/reports/settings.template.json`; `SessionEnd` now carries only `okstra-team-reconcile.sh --session-end`.
- **What cleanup does — okstra's own worker panes**: `okstra team reclaim --project-root <root> --run-manifest <path>` closes the cmux panes this run opened for its workers, and only those whose dispatch has reached a terminal status. The candidates are the `paneId` values recorded in `team-state.workerDispatches[]`, so nothing is discovered by scanning: a title sweep would be worse than nothing, because cmux labels its own agent surfaces with the same glyph the harness uses for a teammate pane and a sweep could close the lead (`team._reclaimable_panes`). A pane the harness opened for its own teammate (`runner: native-session`, which okstra's dispatcher refuses and hands to the host) carries no recorded id and is not okstra's to close. `--dry-run` prints the same `<paneId>\t<kind>` set without closing, which is how the lead counts before it closes. At every worker round boundary — after collecting that round's results and token usage, immediately before the next dispatch — the lead runs the counting pass, reports `PROGRESS: phase-batch-cleanup panes=<n>`, then runs the closing pass. Closing also gives the lead its width back: cmux hands freed width to a neighbour it picks, and that neighbour is not always the lead. See [ADR-0012](../.project-docs/adr/0012-pane-reclaim-scope.md).
- **Cleanup survives compaction and precedes user gates**: A `SessionStart` hook with matcher `compact` (`okstra-compact-reminder.sh`, seeded in `settings.template.json`) fires after every `/compact` or auto-compaction. It reads `cwd` from the hook's stdin JSON and asks `pane_reclaim.in_flight_run_dirs` for that project's runs that still hold a non-terminal dispatch — the newest `team-state` per run directory decides, because sequences accumulate and an earlier round's `in-progress` rows stay on disk. If one exists it prints a factual reminder naming the run dir and the two-pass `okstra team reclaim` sequence, restoring the boundary/gate obligation that a compaction summary can otherwise drop. The hook never closes a pane itself; the closing stays with the lead, which holds the run manifest. The signal is the run directory rather than `~/.okstra/active.jsonl`: an in-session run is registered `prepared` (because `render-bundle` forces `--render-only`), `prepared` routes to `recent.jsonl` as terminal, and that ledger was measured empty while 310 runs sat in `recent.jsonl` — see [ADR-0011](../.project-docs/adr/0011-in-flight-run-source-of-truth.md). Separately, the lead runs the same two passes immediately before any user approval/clarification/decision gate (`PROGRESS: phase-gate-cleanup panes=<n>`). At every cleanup point the pane close and the task stop (`TaskStop`) are paired — a `TaskStop` alone idles the roster task and closes no pane.
- **User confirmation at phase end**: At the final step of the run, the lead calls `okstra team teardown --project-root <root> --run-manifest <path> --dry-run` to show the panes this run recorded, then asks once whether to "close all and clean up teammates / keep them." Unlike the round boundary, teardown's set is every recorded pane regardless of status — the run is ending, so none is expected to continue — and it also writes off any dispatch that never reached a terminal status. If approved, the lead runs the same command without `--dry-run`, then uses `okstra-team-reconcile.sh` to mark dead-pane members inactive and sends each completed teammate a `SendMessage` shutdown_request (`TeamDelete` was removed in v2.1.178; the implicit team disappears with the session). A `cli-wrapper` run recorded no pane, so the command safely reports an empty set. The teammate step is determined by the existence of an on-disk team configuration whose `leadSessionId` matches (`~/.claude/teams/session-*/config.json`), not by `teamCreate.status`.
- Disk accumulation is handled by the `okstra-inspect logs` flow, which offers a read-only inventory and suggests cleanup commands for the user to copy and paste.

### Linked-worktree `.git/` write permissions

- Inside a `--executor codex|antigravity` worktree, `git add` / `git commit` must write to main-repository per-worktree metadata (`<main-repo>/.git/worktrees/<name>/index`, refs, HEAD) and the shared object database (`<main-repo>/.git/objects/`). These paths are outside the worktree directory, so opening only the worktree path in the sandbox causes index.lock creation to fail with EPERM, preventing the executor from satisfying the step-commit contract and forcing it to revert edits and exit.
- `write_scope` in `scripts/okstra_ctl/worker_request.py` resolves the main repository's absolute `.git/` path with `git -C <worktree> rev-parse --git-common-dir` and appends it to the scope, after the project root and the stage tree. The order is contract: a strategy translates the tuple into repeated `--add-dir` positionally. Every provider is told the scope the same way — codex names the project root with `-C` and skips the repeat, and the resolution itself is shared rather than copied per wrapper.
- The scope is what a worker may *write*; where it *runs* is a separate answer that each strategy gives (`ExecCommand.cwd`). A dispatch that passed only the working directory would produce an argv with no `--add-dir` at all, and the worker would then fail by quietly not writing files rather than by erroring.

## Token usage and cost accounting

Tokens used in each run are collected from lead/worker session transcripts and written back to `leadUsage` / `workers[].usage` in `team-state.json`.

- Helper CLI: `scripts/okstra-token-usage.py`
- Collection sources:
  - Claude lead/workers: per-message `message.usage` in `~/.claude/projects/<cwd-as-dashes>/<sessionId>.jsonl` or `~/.claude/projects/<cwd-as-dashes>/<lead-session>/subagents/agent-a<worker-name>-<hash>.jsonl`. Worker names are recovered from nested-subagent filenames, and only the directory for the current run's `team-state.lead.sessionId` is counted.
  - Codex CLI: final `total_token_usage.total_tokens` in `~/.agent/sessions/Y/M/D/rollout-*.jsonl`
  - Antigravity CLI: per-message `tokens.total` in `~/.antigravity/tmp/*/chats/session-*.json`
- CLI execution evidence and token attribution are independent. A wrapper `.status.json` proves `not-started`, `started`, `exited`, `timeout`, or `failed` and supplies the worker's collection window; only a matching transcript with a final token snapshot proves attributable usage. If a wrapper exited successfully but no attributable transcript exists, the worker remains `source: "unavailable"` with `cliExecutionStatus: "exited"` and a reason instead of becoming zero usage or being described as never invoked.
- Records billable-equivalent token math and USD cost estimates. It applies Anthropic billing ratios (`cache_creation_5m=1.25x`, `cache_creation_1h=2.0x`, `cache_read=0.1x`, `output=5x`). When the transcript provides separate `usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens` values, they are counted separately.
- Pricing is centrally managed in `scripts/okstra_token_usage/pricing.py`. Update it when model prices change. Model IDs that fail price matching are exposed to the user in `usageSummary.unmatchedModels`, preventing silent-zero incidents.
- Project-wide historical usage is exposed through the read-only `okstra usage-report` command (`src/commands/inspect/usage-report.mts` → `scripts/okstra_ctl/usage_report.py`) and the `okstra-usage` skill. It defaults to the whole current project's last 30 days and returns run coverage, raw and billable-equivalent tokens, known USD cost, CPU-sum milliseconds, and wall-clock milliseconds grouped by task type. Runs without usable Phase 7 usage are excluded from resource totals and reported through unavailable reason counts rather than treated as zero usage; unmatched model names remain visible when their tokens and time are included but their cost is not. Use `okstra-inspect` for one task's elapsed/context detail and `okstra-rollup` for task-group or project status/report digests.
- **Incremental scan cache (P6)**: To avoid rescanning session jsonl files, a per-file byte cursor and the extracted usage events before windowing are stored in `$OKSTRA_HOME/cache/token-usage/<transcript-dir>/<sessionId>.json` (`scripts/okstra_token_usage/cursor.py`). The run window (since/until) is reevaluated over events on every invocation, so even if a rerun narrows the window, the total matches a full scan. The cache is derived data; identifier mismatch, truncation, or corruption automatically falls back to a full rescan, and `okstra-token-usage.py --no-cache` forces a bypass.
- **Phase timeline (P0 instrumentation)**: The collector extracts `PROGRESS: phase-*` checkpoint lines (see "Progress reporting" in prompts/lead/okstra-lead-contract.md) from the lead session jsonl scoped to the run window, and records them in team-state as a `phaseTimeline` block (`{source, phases: [{phase, firstAt, lastAt, markerCount, wallMsToNext}]}`) (`scripts/okstra_token_usage/collect.py :: phase_timeline`). This provides measurement points for per-phase wall-clock time within the run and is consumed by the "Per-run phase breakdown" in the `okstra-inspect` time facet. Runs without markers explicitly report that measurement is unavailable with `phases: []`.

## Validators

Entry points that enforce whether phase artifacts may be published:

- `validators/validate-workflow.sh` — integrated phase-contract validation.
- `validators/validate-run.py` — version-aware run-level final-report contract. For schema v2 it validates the compact full reading copy heading order and rejects human-only fields in Markdown; task deliverables and cross-field rules are validated from data.json. For schema v1 it retains the Verdict Card, Index, numbered-section, Plan Body Verification, approval, and token-table checks. Both versions require rationale evidence, consistent task verdicts, worker-result audit sidecars, and phase-safe execution evidence.
- `validators/validate-report-views.py` — checks form-control placement in the self-contained HTML view, absence of external URLs, stale source digests, and Response ID parity (`C-*` ↔ HTML).
- `validators/validate-brief.py` — enforces the brief schema (frontmatter, presence of `Reporter Confirmations`, root parent-id self rule, slug conventions, and so on). `bash validators/validate-brief.sh <brief.md>` is a thin wrapper.

Each validator blocks the phase with a `contract-violated` exit code when a contract is breached. Violations are applied when the next phase executes, leaving prior artifacts unchanged.

**Blocking failures vs advisories.** `validate-run.py` splits its findings into `validation.failures` and `validation.advisories` (both written to the run manifest and team state, and printed on stdout; advisories also go to stderr one per line). Only `failures` decide `validation.status`, so a run can pass while carrying advisories. The dividing line is whether a remedy still exists at Phase 7: a defect in the report is fixable by re-authoring and re-rendering, while a defect in what an *already-ended session* wrote down is not — the worker that would restate its citation and the lead pane that would emit its PROGRESS line are both gone, so the only remaining move is to void the phase, which blocks the next one without establishing that any conclusion in the report is wrong. Two surfaces are advisory today: the worker citation ledger (`okstra_ctl.worker_audit_ledger.worker_results_audit_findings` — a `path:line` citation with no matching Evidence read row; a missing or unreadable sidecar stays blocking) and the PROGRESS narration lines (`validators/validate_session_conformance.py` `SessionConformanceResult.advisories`; the user-confirmation cross-check stays blocking because it verifies a claim the report makes). **Enforced:** `tests/contract/test_worker_audit_check.py::test_an_unmatched_citation_is_advisory_and_a_missing_sidecar_is_not` and `tests/contract/test_validate_session_conformance.py::test_missing_checkpoints_are_advisory_not_blocking`.

`contractValidation.status=failed` means the run artifact contract failed; it does not necessarily mean the implementation itself is incomplete. An implementation run derives `phaseOutcome.implementation` from `runs/implementation/carry/stage-<N>.json`, `runs/implementation-planning/consumers.jsonl`, and the approved Stage Map together. If every Stage Map stage has pass-grade carry evidence, it can retain the contract failure as audit information while raising `workflow.nextRecommendedPhase.status` to `ready`. It does not choose the phase: the reconciler's fact is that every stage finished, and the destination was already settled by the last stage's report routing, so it leaves `phase` untouched and raises the status only when `phase` is non-empty.

## Practical notes

- `okstra` is not the old brief-less workflow.
- The brief is the canonical analysis input. Include additional material inline or by path in the brief's `Evidence and Source Materials` section; there is no separate CLI option for supplementary material.
- The brief's `Configuration References and Expected Values` and `Deployment Manifests and Expected Values` sections are the canonical source for task-specific expected state.
- `task-type` also determines profile selection.
- `--render-only` is for dry-run validation, but it still creates the task bundle and run manifest.
- The default execution mode is interactive handoff, not Claude print-mode collection.
- The default final report template is rendered to the task bundle's `instruction-set/final-report-template.md`.
- The task bundle's `instruction-set/reference-expectations.md` is generated alongside it as the config/deployment expected-state reference.
- The current run session's resume helper is created at the resolved `<run-dir>/sessions/claude-resume-<task-type>-<seq>.sh`.
- The run directory is organized into typed subdirectories such as `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, and `worker-results/`; prompt snapshots are prepared under `prompts/` first.
- The host adapter creates workers and collects results according to persisted runner assignments.
- The standard policy uses Claude and Codex analysers plus a report writer. Antigravity, Grok, and Kimi are optional and included only when the selected profile allows them.
- Worker models can be overridden through the legacy provider flags or generic `--worker-model provider=model`; lead and report-writer provider choices are explicit. Defaults are centrally managed through the provider registry and `OKSTRA_DEFAULT_*` environment variables.
- For `--task-type implementation`, select the provider that takes the Executor role with `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`). Only the Executor may mutate project files. The other two providers and the Executor's own provider are each dispatched as verifiers in separate sessions (session isolation preserves the self-review safeguard). The Executor's model reuses the selected provider's worker-model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). Provider, display name, model, `modelExecutionValue`, `hostModelValue`, runner, and dispatch mode are recorded in `teamContract.executor`; there is no provider-specific `workerAgent` field.
- Worktree targeting is runner-based. A native-session executor uses its host adapter's edit and command primitives against `EXECUTOR_WORKTREE_PATH`; a CLI-backed executor receives the worktree path through the deterministic provider process. Prefer explicit working-directory flags such as `git -C` or `cargo --manifest-path` when available.
- The project-level current-task convenience pointer is `.okstra/discovery/latest-task.json`.
- The project-level canonical task inventory is `.okstra/discovery/task-catalog.json`.
- At `okstra install` time, okstra skill assets are seeded to `~/.agents/skills/` by default. If `~/.claude` exists, `~/.claude/skills/` + `~/.claude/agents/` are seeded as well (per-project seeding is no longer performed).
- Seeded okstra Claude assets instruct Claude to dispatch workers into the session's implicit team with `Agent(name: ...)` (v2.1.178 removed `TeamCreate`/`TeamDelete`). Agent targets receive only skill Markdown.
- The host-native Okstra lead, not preparation scripts or a report writer, makes the final judgment.
- A stable task key must be maintained to enable later bug tracking, corrections, and reverification.
- Worker errors are collected in the optional sidecar at the resolved `<run-dir>/logs/errors-<task-type>-<seq>.jsonl`, with the lead as the sole writer. For a stage-isolated run, `<run-dir>` includes `stage-<N>/`. The entry-point helper is `scripts/okstra-error-log.py`.
- Token-usage and cost accounting are handled by `scripts/okstra-token-usage.py` and the Node wrapper `okstra token-usage`.
- `okstra.sh` requires an absolute projectRoot to anchor worker CLI calls.
- `okstra wizard step` **requires** `--answer <val>`. To peek at the next prompt when it is not time to respond, use `--no-submit`.
- `/okstra-inspect history` supports task-manifest fallback / pagination / filters, and resolves `--base-ref` from the worktree registry.
- All writes to the user's project occur only inside `<PROJECT_ROOT>/.okstra/` (see the Artifact-home rule).

## Related documents

- `README.md`
- `templates/reports/task-brief.template.md`
- `templates/reports/final-report-v2.template.md`
- `prompts/lead/okstra-lead-contract.md`
- `scripts/okstra-error-log.py`
- `scripts/okstra-token-usage.py`


---

## okstra Control Center — Operating model / concurrency / environment variables

### Operating model

- At startup, `okstra.sh` invokes the `record_start` hook once to record metadata and the invocation in the indexes.
- Finalization is handled by lazy reconciliation invoked from every `okstra-ctl` entry point (inferred from the presence of `final-report-*.md` in the target project).
- A batch rerun spawns one detached tmux session per target and returns immediately (fire-and-forget). The user connects to any session with the returned attach command.
- The default spawn threshold is 10. Change it with `--max-spawn N` or `OKSTRA_CTL_MAX_SPAWN`.
- runId format: `<project-id>/<task-group>/<task-id>/<task-type>/r<run-seq>` (for example, `sample-project/payment/fail/error-analysis/r07`). Input supports prefix-substring matching.

### Concurrency control (two-level mutex)

`okstra-ctl` and the `record_start` hook use two fcntl `LOCK_EX` locks with different scopes. Their responsibilities are separate, so no deadlock occurs even when both are held.

| Lock | Location | Scope | Held during | Purpose |
|---|---|---|---|---|
| central lock | `~/.okstra/.lock` | Global (index-wide) | Writes to `active.jsonl`/`recent.jsonl`/`projects/<id>/index.jsonl` by `record_start`; reconcile / reservation writes by ctl | Serializes read-modify-write operations on index jsonl files. Prevents concurrent record_start append and reconcile rotation from leaking into each other. |
| task lock | `~/.okstra/.locks/<projectId>-<taskGroup>-<taskId>-<taskType>.lock` | Per task-key + task-type mutex | Entire sequence prediction + tmux spawn section of `okstra-ctl rerun` | Prevents simultaneous reruns of the same task from receiving the same run sequence and colliding on manifests/directories. Acquired outside the central lock, so reruns of different tasks do not block each other. |

Acquisition order is always `task lock` → `central lock` (ctl rerun), or `central lock` alone (record_start, reconcile). The reverse order never occurs.

`task_lock_filename` normalizes each segment to an fs-safe slug, escapes `-` as `--`, and joins with `-`. This prevents tuples such as `('p','feature-8','email','x')` and `('p','feature','8-email','x')` from colliding on the same filename and unintentionally sharing a mutex ([scripts/okstra_ctl/locks.py](../scripts/okstra_ctl/locks.py)).

### Environment variables

- `OKSTRA_HOME`: overrides the central directory location (default `~/.okstra`).
- `OKSTRA_CTL_MAX_SPAWN`: default threshold for concurrent rerun spawns.
- `OKSTRA_CTL_SKIP_BACKFILL=1`: skips automatic backfill on first invocation.
- `OKSTRA_CTL_SKIP_RECONCILE=1`: skips lazy reconciliation (for tests/debugging).
- `OKSTRA_SKIP_INSTALL_CHECK=1`: skips installation-asset checks in `verify_installation` (for tests; workspace-existence validation remains enabled).
- `OKSTRA_RUN_SEQ_OVERRIDE`: forces the run sequence used by okstra.sh during a rerun (injected automatically inside okstra-ctl).
