# Optional project configuration (okstra-setup)

Use this reference only when the user asks to customise one of the areas below after
`project.json` exists. The built-in defaults work for most projects — none of
these sections is required for a working setup.

The Bash invocation rule from SKILL.md applies to every command here.

## A. Worktree sync dirs (`worktreeSyncDirs`)

Each okstra run provisions a task-scoped git worktree under
`~/.okstra/worktrees/.../`. A small set of project-root-relative directories is
symlinked from the main checkout into that worktree so every task sees the
shared state. The built-in default is `.project-docs`, `.scratch`,
`graphify-out`, `.claude`. Syncing a directory does not make it okstra memory;
okstra-owned context and writes still stay under `<PROJECT_ROOT>/.okstra/**`
unless the task brief explicitly authorizes a non-okstra path.

`.claude` is the one entry materialised as a real directory whose children are
symlinked one by one, instead of a single symlink for the whole directory. git
does not follow a symlink, so a symlinked directory is one file to it, and a
project that ignores host config by its contents (`.claude/*`) matches every
child but never the bare `.claude` path — the directory would be invisible in
the main checkout and `?? .claude` in the task worktree. Linking the children
reproduces the main checkout's shape, so the project's own ignore rules decide
the outcome in both places. The other entries stay whole-directory symlinks
because okstra writes into them, and the directory symlink is what makes a
newly created top-level entry land in the main checkout.

To override per-project, add a `worktreeSyncDirs` array to `project.json`.
Empty array disables the feature; the field is preserved across the runtime's
auto-upserts (only `projectId`, `projectRoot`, `createdAt`, `updatedAt` are
runtime-owned).

```json
{
  "projectId": "...",
  "projectRoot": "...",
  "worktreeSyncDirs": [".project-docs", ".scratch", "graphify-out", ".claude", "my-custom-dir"]
}
```

Resolution precedence: `OKSTRA_WORKTREE_SYNC_DIRS` env var → this field →
built-in default. Only edit when defaults don't cover the project's working
files (e.g. additional cache or local-config dirs that must follow the
executor into the worktree).

## B. Project QA commands (`qaCommands`) — recommended

`implementation`-phase verifiers run an independent QA gate over the
executor's diff and need a project-wide baseline of check-only lint / format /
typecheck / test commands. okstra does NOT auto-detect tooling — declare the
commands explicitly in `project.json` under `qaCommands`. Skipping this is
allowed but the verifier will then only run the plan's per-task `validation`
set, with `qa-command not configured: <category>` recorded per missing
category in the final report.

Each category is an array of `{ "label", "cmd", "language"? }` objects.
`language` is optional; when present the verifier MAY skip commands whose
language is not represented in this run's diff.

```json
{
  "projectId": "...",
  "projectRoot": "...",
  "qaCommands": {
    "lint":      [{ "label": "cargo clippy", "cmd": "cargo clippy --all-targets -- -D warnings", "language": "rust" }],
    "format":    [{ "label": "cargo fmt",    "cmd": "cargo fmt --check",                          "language": "rust" }],
    "typecheck": [{ "label": "tsc",          "cmd": "pnpm exec tsc --noEmit",                     "language": "ts"   }],
    "test":      [{ "label": "cargo test",   "cmd": "cargo test --workspace --locked",            "language": "rust" }]
  }
}
```

**`cmd` deny-list (mutation guard):** the verifier rejects any `cmd`
containing tokens that imply mutation — declare commands in their check-only
form only. Representative denied tokens: `--fix` / `--write` (formatters),
`--updateSnapshot`, `npm install` (use `npm ci`), `pip install -U`,
`cargo add`. The authoritative deny list is
`scripts/okstra_ctl/qa_commands.py::find_denied_tokens`
(`_DENIED_LITERAL_TOKENS` / `_DENIED_SUBSTRINGS` plus the dynamic
`npm install` and `INSTA_UPDATE=` checks) — do not re-enumerate it here.

Encountering a denied token aborts the verifier with status
`contract-violated`; re-declare the command in check-only form to recover
(e.g. swap `prettier --write` → `prettier --check`).

The field is preserved across the runtime's auto-upserts of `project.json`, so
manual edits to `qaCommands` survive every subsequent `okstra setup` /
`okstra run` invocation.

### B.1 `qaEnv` — Tier 3 conformance environment

`implementation` / `final-verification` verifiers run **stage conformance
scripts** (Tier 3) that may need to reach a database or an HTTP endpoint to
prove the diff satisfies upstream requirements. Declare the environment those
scripts are allowed to touch under `qaEnv`. Every field is optional; declare
only what your conformance scripts use.

`qaEnv` lets Okstra attempt Tier 3 automatically; it does not transfer
ownership of a real DB/API check to the run. Entries requiring `db`, `http`,
or `external` are advisory: a missing environment or non-PASS result is
reported with an exact rerun method but does not block the run. `io`-only
conformance stays blocking. Remote IO should also declare `external`; this is
an authoring guideline, not an automatically detected contract. Gate outcomes
are enforced by `scripts/okstra_ctl/conformance.py::decide_conformance_gate`
and `validators/validate-run.py::_validate_conformance`.

```json
"qaEnv": {
  "replicaDbDsn": "<replica/test DB DSN — never shared/staging/prod>",
  "appBaseUrl": "http://localhost:3000",
  "envFile": ".okstra/qa.env",
  "surfacePatterns": { "db": ["*.sql", "*repository*"], "http": ["*controller*"] }
}
```

- `replicaDbDsn` — DSN the conformance script connects to. MUST be a replica /
  disposable test DB, **never** a shared, staging, or production database
  (conformance scripts may write).
- `appBaseUrl` — base URL for endpoint-level conformance checks (local app
  only).
- `envFile` — path (under `.okstra/`) to an env file the verifier sources
  before running conformance scripts.
- `surfacePatterns` — per-project **override** of the diff-surface cross-check
  map (`capability → glob list`). The validator maps each changed file to a
  capability surface (`db` / `http` / `io`) and fails the declaration contract
  when the diff touches a surface no stage `requires`. The built-in patterns
  (e.g. `*router*` for `http`, `*storage*` for `io`) are broad and match many
  front-end files, so front-end-heavy repos should override with narrower globs
  to avoid false BLOCKING verdicts. An over-broad pattern over-blocks; an
  over-narrow one lets an undeclared surface through — tune to the repo's real
  db/http/io file naming.

## C. Project-local Claude settings symlink and workspace trust

`okstra setup` (and `okstra run` on its first invocation per project)
provisions `<PROJECT_ROOT>/.claude/settings.local.json` as a symlink to
`~/.okstra/templates/settings.local.json`. The template contains the Bash
permission rules required for the codex/antigravity worker wrappers:

- `Bash($HOME/.okstra/bin/okstra-codex-exec.sh:*)`
- `Bash($HOME/.okstra/bin/okstra-antigravity-exec.sh:*)`

Claude Code loads `.claude/settings.local.json` inside that project after the
project has been trusted. When setup creates this file during an already-open
Claude Code session, newly dispatched worker processes can each encounter the
workspace-trust prompt before that host session has accepted the project.

`/okstra-run` therefore checks the **host runtime** before starting its wizard.
For a Claude Code host with pending trust, it stops before any worker dispatch
and asks the user to close the session, reopen the project, accept the single
workspace-trust prompt, and rerun `/okstra-run`. This is not a per-worker or
per-provider approval: selecting Claude, Codex, or Antigravity workers does not
change the host-runtime check. Codex and external hosts do not read Claude Code
workspace state.

If a non-symlink `.claude/settings.local.json` already exists, setup backs it
up to `.claude/settings.local.json.bak.<timestamp>` before installing the
symlink — surface that to the user so they can merge project-specific rules
back (the symlinked template is okstra-owned and refreshed on okstra updates).
To opt out (advanced): replace the symlink with a regular file; okstra will
back it up as `.bak.<timestamp>` on its next setup call rather than
overwriting silently.

## D. Project PR body template (release-handoff)

`release-handoff` fills the PR body from a template — by default
`~/.okstra/templates/pr/pr-body.template.md`. Most projects want their own
(e.g. `.github/PULL_REQUEST_TEMPLATE.md`). Pre-registration during setup is
opt-in; the same prompt is offered again on the first `release-handoff` run,
so deferring (`Later`) is safe.

Ask with `AskUserQuestion` (fixed options — file path entry happens in the
follow-up plain text prompt):

- **Question**: `"Register the PR body template that release-handoff will use for this project?"`
- **Options**:
  1. `This project only (project scope)` — writes `prTemplatePath` to `<PROJECT_ROOT>/.okstra/project.json`.
  2. `Global (global scope)` — writes `prTemplatePath` to `~/.okstra/config.json`.
  3. `Later` — skip.

If scope 1 or 2, follow up with a plain text prompt:
`"Tell me the PR body template file path. project scope accepts a project-root-relative or absolute path; global scope accepts only an absolute path or a ~/ path."`
Consume the next user message, then run:

```bash
okstra config set pr-template-path "<typed-path>" --scope <project|global>
```

The command validates the value (global rejects relative paths) and writes
atomically. Surface its fixed text result. If the user chose `Later`, tell them
they can register later via the same `okstra config set` command or the
per-run override prompt during the next release-handoff run.

## E. Final report language (`reportLanguage`)

The default is English. If you skip, no `reportLanguage` field is written and
the runtime treats it as `auto` — it follows the task brief's primary
narration language (Korean brief → Korean report, English brief → English
report).

AskUserQuestion (fixed options):
- Question: `"Which language should the final report be written in?"`
- Options:
  1. `English (recommended)` → `en`
  2. `Korean` → `ko`
  3. `Auto (infer from the task brief language; English if unclear)` → `auto`
  4. `Later` → skip (no field set → runtime treats it as auto)

If the user picks 1/2/3:

```bash
okstra config set report-language <en|ko|auto> --scope project
```

Set the global default manually with `--scope global` as described in the
README's "global config" guidance — this flow offers only project scope.

## F. Declared architecture style (`architecture.style`)

`architecture.style` declares the project's architecture — one of
`hexagonal`, `layered`, or `none`. It is optional and defaults to `none`: an
absent field, an unrecognized value, or an unreadable `project.json` all
resolve to `none`
(`scripts/okstra_project/resolver.py::resolve_architecture`), so a project
that never declares one keeps today's behaviour unchanged.

Like `worktreeSyncDirs` and `qaCommands`, `okstra setup` does NOT write this
field — hand-add it to `project.json`. It is preserved across the runtime's
auto-upserts (only `projectId`, `projectRoot`, `createdAt`, `updatedAt` are
runtime-owned), so the manual edit survives every subsequent `okstra setup` /
`okstra run` invocation.

```json
{
  "projectId": "...",
  "projectRoot": "...",
  "architecture": { "style": "hexagonal" }
}
```

Declaring `hexagonal` or `layered` promotes that architecture's placement
rules from advisory to a binding planning + verification constraint:

- `hexagonal` — extraction itself stays a free decision (`extract: false`
  remains legal), but a variation point `implementation-planning` *does*
  extract has to sit behind a port: an `extractionDecision` carrying
  `extract: true` with any `interfaceKind` other than `"port"` fails
  `validators/validate-run.py::_validate_variation_point_analysis`. The
  implementation executor loads the `architectures/hexagonal.md` preflight
  pack even when directory-shape detection did not match it, and the verifier
  grades a service dependency the diff adds or modifies that injects a
  concrete adapter instead of a port as a blocking `FAIL` rather than a
  recommendation.
- `layered` — no preflight pack resource. Its binding invariant is dependency
  direction: an upper layer may import a lower one, never the reverse. A
  reverse import is a blocking placement violation found by worker judgement,
  because no machine check reads layer names.
- `none` (or the field left out) — the style-agnostic planning rules still
  run on every plan (variation-point analysis and test seams); only the
  placement overlay stays advisory and detection-driven.

The full two-layer model lives in `docs/architecture.md` § Project
self-registration.

## G. Project review rule packs (`reviewRulePacks`)

`reviewRulePacks` declares the review standards this project's phases read
before they judge a plan or a diff — a team PR-review skill's `SKILL.md`, for
example. Without it a pack reaches a run only when the task brief cites its
exact path, so whether the team standard applied came down to who wrote the
brief. A declared pack applies to every run in the project; the brief citation
keeps working, and the two channels are a union.

`okstra setup` never writes it. Hand-add it to `project.json` and the upsert
preserves it:

```json
{
  "reviewRulePacks": [
    "/Users/me/.claude/skills/team-pr-reviewer/SKILL.md"
  ]
}
```

- **Absolute paths only.** A worker runs with a task worktree as its cwd, so a
  relative path names a different file there — relative entries are dropped by
  `scripts/okstra_project/resolver.py::resolve_review_rule_packs`, which also
  falls back to "none declared" on an unreadable or malformed `project.json`.
  A leading `~` is expanded.
- **Who reads it.** `implementation-planning` (to plan away the findings before
  the code exists), the implementation executor's coding-conventions preflight,
  and the static review passes of the implementation verifier and
  `final-verification`. Each reads only the declared file and the
  `references/*.md` files it directly names — never a parent directory or a
  host skill catalog.
- **What is machine-checked.** `okstra doctor --phase <phase>` fails its
  `review rule packs` check when a declared path is not a readable file,
  because a stale path otherwise costs the whole pack in silence: the phase
  records that it read no rules and the run still passes. Whether a pack that
  does resolve was actually read and applied stays the phase's own
  `project-review-rules:` record — no machine check reads a worker's reasoning.
