okstra 0.121.1 → 0.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/docs/architecture.md +4 -2
  2. package/docs/project-structure-overview.md +1 -1
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/workers/claude-worker.md +1 -1
  6. package/runtime/prompts/launch.template.md +4 -4
  7. package/runtime/prompts/lead/adapters/claude-code.md +104 -0
  8. package/runtime/prompts/lead/adapters/codex.md +45 -0
  9. package/runtime/prompts/lead/adapters/external.md +46 -0
  10. package/runtime/prompts/lead/context-loader.md +5 -5
  11. package/runtime/prompts/lead/convergence.md +11 -28
  12. package/runtime/prompts/lead/okstra-lead-contract.md +42 -92
  13. package/runtime/prompts/lead/plan-body-verification.md +21 -5
  14. package/runtime/prompts/lead/report-writer.md +50 -51
  15. package/runtime/prompts/lead/team-contract.md +33 -106
  16. package/runtime/prompts/profiles/_common-contract.md +2 -2
  17. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  18. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  19. package/runtime/prompts/profiles/implementation-planning.md +3 -3
  20. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  21. package/runtime/python/okstra_ctl/lead_runtime.py +4 -0
  22. package/runtime/python/okstra_ctl/render.py +76 -194
  23. package/runtime/python/okstra_ctl/report_views.py +22 -7
  24. package/runtime/python/okstra_ctl/team_reconcile.py +3 -1
  25. package/runtime/schemas/final-report-v1.0.schema.json +3 -0
  26. package/runtime/skills/okstra-schedule/SKILL.md +60 -18
  27. package/runtime/templates/reports/final-report.template.md +4 -4
  28. package/runtime/templates/reports/i18n/en.json +1 -1
  29. package/runtime/templates/reports/i18n/ko.json +1 -1
  30. package/runtime/validators/validate-run.py +89 -0
  31. package/runtime/validators/validate-schedule.py +7 -3
  32. package/runtime/validators/validate_session_conformance.py +58 -33
  33. package/src/cli-registry.mjs +7 -0
  34. package/src/commands/inspect/stage-map.mjs +100 -0
  35. package/src/commands/lifecycle/install.mjs +3 -0
  36. package/src/commands/lifecycle/uninstall.mjs +8 -23
  37. package/src/lib/skill-catalog.mjs +8 -0
@@ -9,7 +9,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
9
9
 
10
10
  ## Verifier roles (resolved at run-prep time)
11
11
 
12
- - **Verifier dispatch labelling.** Agent `name` = `<provider>-verifier` (here, and identically in `final-verification`); for a `codex` / `antigravity` verifier, Lead additionally injects `**Pane role:** verifier` into the dispatched prompt body. Naming/mapping rules and the token-attribution guarantee are owned by `prompts/lead/okstra-lead-contract.md` Phase 4 "Agent `name` on dispatch".
12
+ - **Verifier dispatch labelling.** The core functional role label is `<provider>-verifier` (here, and identically in `final-verification`). Provider, role, and model identity are owned by `prompts/lead/okstra-lead-contract.md` "Model assignments"; the selected runtime adapter owns provider-native dispatch-label mapping (including any `name` / `**Pane role:**` fields) and token-attribution wiring under its "Semantic operation mapping".
13
13
  - The verifier slots are `Claude verifier` and `Codex verifier`, plus `Antigravity verifier` **only when `antigravity` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
14
14
  - Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
15
15
  - Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.6`, `Antigravity verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.6`), antigravity→`--antigravity-model` (default `auto`).
@@ -62,7 +62,7 @@
62
62
  - Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary — not deferred to the `implementation` entry gate. It enforces: the exact `## 5.5 Stage Map` heading, each `## 5.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤8), the `depends-on` DAG, and the per-stage vertical-slice contract (S10). S10 scans for the literal in-section strings `Slice value:`, `Acceptance:`, the three `Test case (success):` / `Test case (boundary):` / `Test case (failure):` lines (S10d), and the Stepwise `action`-cell prefixes `RED:` / `GREEN:` (or a `TDD exemption:` line, which waives both the test-case lines and the RED/GREEN check) — keep these tokens verbatim for the same reason as the heading keywords above.
63
63
  - Required deliverable shape (final report, in addition to the standard sections):
64
64
  - at least two implementation options. **Each option must include**:
65
- - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`.
65
+ - **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`. Write every `path` in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...` / a trailing `/…`); an abbreviated path does not resolve and is rejected by plan-body verification as a kind-b path mismatch.
66
66
  - affected interfaces / public contracts and downstream consumers
67
67
  - estimated blast radius (units, configs, deployment manifests, data migrations)
68
68
  - trade-off matrix across options (rows = options, columns at minimum: complexity, risk, reversibility, test coverage cost, rollout cost)
@@ -81,7 +81,7 @@
81
81
  Validator S10d rejects a missing/empty line in any of the three categories (skipped only when a `TDD exemption:` line is present). The `RED:` step below must encode these cases, not a single assertion.
82
82
  - **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
83
83
  - `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
84
- - `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change (시간 하한 없음; 함께 바뀌는 여러 파일을 포함할 수 있다); for code steps include actual code or diff sketch. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`expected` = FAIL) — the RED step encodes the case set, not a single happy-path assertion; at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`expected` = PASS); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section (mirrors the executor's per-step exemption in `_implementation-executor.md`). Validator S10c enforces RED-first + GREEN **and** that the `RED:` step's `expected` reads FAIL / the `GREEN:` step's reads PASS; S10e rejects a `TDD exemption:` whose reason is not one of doc-only / config-only / pure-rename (both in `validators/validate-implementation-plan-stages.py`).
84
+ - `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change (시간 하한 없음; 함께 바뀌는 여러 파일을 포함할 수 있다); for code steps include actual code or diff sketch. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`expected` = FAIL) — the RED step encodes the case set, not a single happy-path assertion; at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`expected` = PASS); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section (mirrors the executor's per-step exemption in `_implementation-executor.md`). Validator S10c enforces RED-first + GREEN **and** that the `RED:` step's `expected` reads FAIL / the `GREEN:` step's reads PASS; S10e rejects a `TDD exemption:` whose reason is not one of doc-only / config-only / pure-rename (both in `validators/validate-implementation-plan-stages.py`).
85
85
  - **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
86
86
  - `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — a Tier3 verification script that proves this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. When you emit this line you MUST also (a) write the script to `<task_root>/qa/scripts/stage-<N>.<ext>` and (b) add a matching entry to `<task_root>/qa/conformance-manifest.json` with fields `stageKey` (= `<task-id>-stage-<N>`), `script`, `runCommand`, `requirementIds`, `requires` (subset of `{db, io, http, external}`), `passContract`, `exemption: null`, `waiver: null`. The script's standard interface: a `main` that exits `0`=PASS / non-zero=FAIL, and whose stdout ends with `QA-RESULT: PASS|FAIL` followed by one `REQ <id>: PASS|FAIL: <근거>` line per requirement. When the verification body is a test spec, author it with the project's own test framework (devDependency) invoked via a discovery override at `<task_root>/qa/scripts/` (jest: `--config <project config> --roots <task_root>/qa/scripts`) — never hand-roll `describe`/`expect` and never widen the project's own test config; for TypeScript specs also write `<task_root>/qa/scripts/tsconfig.json` extending the project tsconfig with the runner's `types` entry so editors resolve the file.
87
87
  - `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.)
@@ -110,7 +110,7 @@
110
110
  - the YAML frontmatter MUST include the line `implementation-option:` directly under `approved:` (report-writer always emits it with an **empty value**). The user selects which Option Candidate the next `implementation` run executes by filling this line with that option's name (manual edit or `--implementation-option <name>` CLI). When left empty, the `implementation` run falls back to the `Recommended Option`.
111
111
  - **the frontmatter `approved: false` line is rendered unconditionally; if the plan-body verification gate (§5.5.9) returns `blocked-by-disagreement` or `aborted-non-result`, the writer MUST keep `approved: false` and the validator refuses any report that ships with `approved: true` under such a gate result.**
112
112
  - every ambiguity flagged during pre-planning that the user must resolve before approval registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (the unified table is the single home for these — the "no separate `Open Questions` block" rule is in the shared `_common-contract.md` clarification policy)
113
- - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (Option Candidates / Trade-off Matrix / Recommended Option / Stage Map and per-stage sections / Dependency / Validation Checklist / Rollback / Requirement Coverage) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `prompts/lead/plan-body-verification.md`. The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_body_gate_recompute` re-derives the gate from `planItems[].verdicts` and fails when the declared `gateResult` claims a healthier outcome than the recorded votes support; `_validate_plan_item_extraction_completeness` fails when any plan-body deliverable category is under-extracted into `planItems`, so a dropped item can no longer dodge the gate. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture").
113
+ - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (Option Candidates / Trade-off Matrix / Recommended Option / Stage Map and per-stage sections / Dependency / Validation Checklist / Rollback / Requirement Coverage) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `prompts/lead/plan-body-verification.md`. The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_body_gate_recompute` re-derives the gate from `planItems[].verdicts` and fails when the declared `gateResult` claims a healthier outcome than the recorded votes support; `_validate_plan_item_extraction_completeness` fails when any plan-body deliverable category is under-extracted into `planItems`, so a dropped item can no longer dodge the gate. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture"). majority-disagree 항목 중 `planner-fixable` 다수는 사용자 clarification 으로 승격하기 전에 report-writer 자가수정 1회를 거친다(`prompts/lead/plan-body-verification.md` "Self-fix round"). self-fix 로 해소되지 않았거나 `needs-user-input` 다수인 항목만 `Blocks=approval` clarification 이 된다. `validators/validate-run.py` `_validate_self_fix_before_clarification` 가 self-fix 없이 승격된 planner-fixable 다수 항목을 `contract-violated` 로 실패시킨다.
114
114
  - **Decision-record evaluation (sole owner)**: this phase is the **single owner** of decision-record evaluation in the okstra lifecycle. The brief never evaluates or drafts decision records — it only forwards `adr-candidate:*` signals. Every `adr-candidate:*` entry inherited from the brief's `Open Questions` is a mandatory evaluation target. In addition, evaluate every decision the recommended option introduces against the three criteria:
115
115
  1. **Hard to reverse** — would changing the decision later cost meaningfully more than deciding now?
116
116
  2. **Surprising without context** — would a future reader, seeing only the code, wonder "why was it built this way?"?
@@ -42,7 +42,7 @@
42
42
  각 단위를 새 task-key 로 시작한다(이 phase 는 다운스트림 run 을 직접 시작하지 않는다).
43
43
  - Decision-tree walk (bounded):
44
44
  - When the brief's `Desired Outcome`, classification, or routing target depends on a chain of decisions, walk that chain one branch at a time. Each branch is one `Clarification Items` row, not a free-form interview.
45
- - For every clarification row, put the single best answer and one-line rationale in `Expected form` as `Recommended: ...`. Put other options and one-sentence consequences in the same cell as `Alternatives: ...`.
45
+ - For every clarification row, put the single best answer and one-line rationale in `Expected form` as `Recommended: (a) ...`. Put other options and one-sentence consequences in the same cell as `Alternatives: (b) ... (c) ...` — the recommended answer is option `(a)` and alternatives continue from `(b)`, so the labels read `(a) (b) (c) …` in order.
46
46
  - **Codebase-first rule**: if a branch can be resolved by `Read` / `Grep` / file inspection, resolve it that way and record `Evidence checked: <path:line>` in the `Statement` cell. Do NOT escalate to the user.
47
47
  - Budget: the unified `## 1. Clarification Items` table caps at the smaller of (a) one row per unresolved decision branch, (b) 8 rows total. Beyond the cap, fold remaining ambiguity into the routing recommendation's risk notes.
48
48
  - Expected output emphasis:
@@ -14,6 +14,7 @@ class LeadRuntimeInfo:
14
14
  adapter_dispatch_mode: str
15
15
  session_accounting: str
16
16
  has_claude_session: bool
17
+ adapter_contract_relative_path: str
17
18
 
18
19
  def adapter_payload(self) -> dict[str, str]:
19
20
  return {
@@ -36,6 +37,7 @@ _LEAD_RUNTIMES = {
36
37
  adapter_dispatch_mode="team",
37
38
  session_accounting="claude-jsonl",
38
39
  has_claude_session=True,
40
+ adapter_contract_relative_path="lead/adapters/claude-code.md",
39
41
  ),
40
42
  "codex": LeadRuntimeInfo(
41
43
  runtime="codex",
@@ -46,6 +48,7 @@ _LEAD_RUNTIMES = {
46
48
  adapter_dispatch_mode="render-only",
47
49
  session_accounting="artifact-only",
48
50
  has_claude_session=False,
51
+ adapter_contract_relative_path="lead/adapters/codex.md",
49
52
  ),
50
53
  "external": LeadRuntimeInfo(
51
54
  runtime="external",
@@ -56,6 +59,7 @@ _LEAD_RUNTIMES = {
56
59
  adapter_dispatch_mode="render-only",
57
60
  session_accounting="artifact-only",
58
61
  has_claude_session=False,
62
+ adapter_contract_relative_path="lead/adapters/external.md",
59
63
  ),
60
64
  }
61
65
 
@@ -1735,6 +1735,9 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1735
1735
  lead_contract_path = ctx.get("OKSTRA_LEAD_CONTRACT_PATH") or str(
1736
1736
  home_prompts / "lead" / "okstra-lead-contract.md"
1737
1737
  )
1738
+ adapter_contract_path = ctx.get("OKSTRA_LEAD_ADAPTER_CONTRACT_PATH") or str(
1739
+ home_prompts / _lead_info(ctx).adapter_contract_relative_path
1740
+ )
1738
1741
  context_loader_path = ctx.get("OKSTRA_CONTEXT_LOADER_PATH") or str(
1739
1742
  home_prompts / "lead" / "context-loader.md"
1740
1743
  )
@@ -1756,7 +1759,10 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1756
1759
  okstra_runtime_resources_block = (
1757
1760
  "## Okstra Runtime Resources\n"
1758
1761
  "\n"
1759
- f"- Lead contract: `{lead_contract_path}`\n"
1762
+ f"- Lifecycle core contract: `{lead_contract_path}`\n"
1763
+ f"- Selected runtime adapter contract: `{adapter_contract_path}`\n"
1764
+ "- Read the lifecycle core first, then the selected adapter. "
1765
+ "Only the selected adapter may define host tool spelling.\n"
1760
1766
  f"- Context loader: `{context_loader_path}`\n"
1761
1767
  f"- Team contract: `{team_contract_path}`\n"
1762
1768
  f"- Convergence contract: `{convergence_path}`\n"
@@ -1822,193 +1828,97 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1822
1828
  else "- `Antigravity worker` is not selected for this run, so no Antigravity attempt is required."
1823
1829
  )
1824
1830
 
1825
- # Teammate dispatch gate block: phase-conditional. CC v2.1.178 removed
1826
- # TeamCreate/TeamDelete + the Agent team_name param; every session owns one
1827
- # implicit team, so workers dispatch with Agent(name: ...) and no team_name.
1828
- # - release-handoff (and any other phase that renders with no workers
1829
- # selected) is single-lead and dispatches no teammate at all. Emit a
1830
- # short notice instead of the BLOCKING gate.
1831
- # - concurrent implementation runs only record an audit marker (each session
1832
- # has its own implicit team, so there is no shared-team conflict).
1833
- # - All other phases keep the full teammate-dispatch contract.
1834
1831
  task_type = ctx.get("TASK_TYPE", "")
1835
- concurrent_stages = str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").strip()
1836
- codex_dispatch_command = (
1837
- "okstra codex-dispatch "
1838
- f"--project-root {ctx.get('PROJECT_ROOT', '')} "
1839
- f"--run-manifest {ctx.get('RUN_MANIFEST_RELATIVE_PATH', '')}"
1840
- )
1841
- team_dispatch_command = (
1842
- "okstra team dispatch "
1843
- f"--project-root {ctx.get('PROJECT_ROOT', '')} "
1844
- f"--run-manifest {ctx.get('RUN_MANIFEST_RELATIVE_PATH', '')}"
1845
- )
1846
- team_await_command = (
1847
- "okstra team await "
1848
- f"--project-root {ctx.get('PROJECT_ROOT', '')} "
1849
- f"--run-manifest {ctx.get('RUN_MANIFEST_RELATIVE_PATH', '')}"
1850
- )
1851
- if task_type == "release-handoff" or not selected:
1852
- team_creation_gate_block = (
1853
- "## Single-Lead Phase (no team creation)\n"
1854
- "\n"
1855
- "This run is single-lead. There is no worker roster, no\n"
1856
- "`TeamCreate` call, no `Agent(...)` worker dispatch, and no\n"
1857
- f"convergence loop. The {lead_role} performs every step inline\n"
1858
- "(reading inputs, drafting commit / PR text when applicable,\n"
1859
- "asking the user, running git / gh, and writing the final\n"
1860
- "report). Do NOT call `TeamCreate` or dispatch any sub-agent\n"
1861
- "from this run — that would be a contract violation."
1862
- )
1863
- elif lead_runtime == "codex":
1864
- team_creation_gate_block = (
1865
- "## Codex Dispatch Gate (BLOCKING)\n"
1866
- "\n"
1867
- "This run was prepared for `leadRuntime=codex`. Do NOT call\n"
1868
- "`TeamCreate`, do NOT call Claude Code `Agent(...)`, and do NOT\n"
1869
- "invoke Claude slash skills. Codex dispatch is artifact-first and\n"
1870
- "uses the prepared run manifest plus CLI-backed worker wrappers.\n"
1871
- "\n"
1872
- "Required actions, in order:\n"
1873
- "\n"
1874
- "1. Inspect the run manifest and team-state paths listed below.\n"
1875
- "2. Emit `PROGRESS: phase-3-team-create skipped (codex-dispatch)`;\n"
1876
- " there is no Claude Teams surface in this adapter.\n"
1877
- "3. Plan worker execution with:\n"
1878
- f" `{codex_dispatch_command} --dry-run`\n"
1879
- "4. Dispatch supported Codex-side workers with:\n"
1880
- f" `{codex_dispatch_command}`\n"
1881
- " When `--workers` is omitted, unsupported roster entries such as\n"
1882
- " `claude` are skipped with explicit `not-run` reasons.\n"
1883
- "5. Run `report-writer` only with explicit opt-in:\n"
1884
- f" `{codex_dispatch_command} --enable-codex-report-writer --report-writer-codex-model <model>`\n"
1885
- "\n"
1886
- "`okstra codex-dispatch` records `team-state.dispatchMode`, writes\n"
1887
- "worker prompt histories when missing, persists worker results, and\n"
1888
- "runs post-report validation for the Codex report-writer path."
1889
- )
1890
- elif lead_runtime == "external":
1891
- team_creation_gate_block = (
1892
- "## Tmux Worker Dispatch Gate (BLOCKING)\n"
1893
- "\n"
1894
- "This run was prepared for `leadRuntime=external`. Do NOT call "
1895
- "`TeamCreate`, do NOT call Claude Code `Agent(...)`, and do NOT call "
1896
- "`okstra codex-dispatch`. This adapter dispatches workers through "
1897
- "okstra-owned tmux panes.\n"
1898
- "\n"
1899
- "Required actions, in order:\n"
1900
- "1. Inspect the run manifest and team-state paths listed below.\n"
1901
- "2. Emit `PROGRESS: phase-3-team-create skipped (tmux-pane)`; there is no Claude Teams surface in this adapter.\n"
1902
- "3. Plan worker execution with:\n"
1903
- f" `{team_dispatch_command} --dry-run`\n"
1904
- "4. Dispatch workers with:\n"
1905
- f" `{team_dispatch_command}`\n"
1906
- "5. Arm one background wait using this harness's async shell primitive:\n"
1907
- f" `{team_await_command}`\n"
1908
- "6. On wake, read team-state. Worker completion is valid only from "
1909
- "`workerDispatches[]` plus result files; pane creation alone is not completion."
1910
- )
1911
- elif task_type == "implementation" and concurrent_stages:
1912
- team_creation_gate_block = (
1913
- "## Concurrent-run marker (BLOCKING)\n"
1914
- "\n"
1915
- "A concurrent implementation run of the same task-key holds stage(s)\n"
1916
- f"`{concurrent_stages}` right now. Each session owns its own implicit\n"
1917
- "team (`session-<leadSessionId-prefix>`), so concurrent same-task runs do\n"
1918
- "not share team state — this run dispatches normally and only records the\n"
1919
- "concurrent context for the audit trail.\n"
1920
- "\n"
1921
- "Required actions, in order:\n"
1922
- "\n"
1923
- "1. Before any dispatch, record in team-state:\n"
1924
- ' `teamCreate: { attempted: false, status: "skipped",'
1925
- f' reason: "concurrent-run", concurrentStages: [{concurrent_stages}] }}`.\n'
1926
- "2. Immediately after recording it, emit the checkpoint line\n"
1927
- " `PROGRESS: phase-3-team-create skipped (concurrent-run)` — the\n"
1928
- " phase-3 checkpoint is required and the session-conformance validator\n"
1929
- " fails the run without it.\n"
1930
- "3. Dispatch every worker with `Agent(name: ..., run_in_background: true)`\n"
1931
- " and NO `team_name`; teammates join this session's implicit team and\n"
1932
- " render as split-panes when `$TMUX` is set."
1933
- )
1934
- else:
1832
+ adapter_setup_facts: list[str] = []
1833
+ if lead_runtime == "claude-code":
1935
1834
  base_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1936
1835
  impl_stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1937
1836
  fv_stage = str(ctx.get("RUN_STAGE", "") or "").strip()
1938
1837
  stage_suffix = ""
1939
1838
  if task_type == "implementation" and impl_stage:
1940
- # stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
1941
- # team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
1942
1839
  stage_suffix = f"-s{impl_stage}"
1943
1840
  elif task_type == "final-verification" and fv_stage:
1944
- # 단일-stage final-verification 도 stage 별 team. `-fv-` 를 끼워
1945
- # 같은 stage 의 implementation team(`-s<N>`)과도 구분한다 — 둘은
1946
- # 동시에 살아 있을 수 있다. whole-task 검증(RUN_STAGE 빈 값)은
1947
- # 기본 이름 유지.
1948
1841
  stage_suffix = f"-fv-s{fv_stage}"
1949
- team_name = sanitize_team_name(base_name, stage_suffix)
1842
+ adapter_setup_facts.append(
1843
+ f"- Adapter audit label: `{sanitize_team_name(base_name, stage_suffix)}`"
1844
+ )
1845
+ concurrent_stages = str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").strip()
1846
+ if concurrent_stages:
1847
+ adapter_setup_facts.append(
1848
+ f"- Prepare-recorded concurrent stages: `{concurrent_stages}`"
1849
+ )
1850
+ adapter_setup_block = (
1851
+ "\n".join(adapter_setup_facts)
1852
+ if adapter_setup_facts
1853
+ else "- Adapter setup facts: none"
1854
+ )
1855
+
1856
+ if task_type == "release-handoff" or not selected:
1950
1857
  team_creation_gate_block = (
1951
- "## Teammate Dispatch Gate (BLOCKING)\n"
1952
- "\n"
1953
- "Before any `Agent` dispatch for workers, you MUST perform Phase 3 of the\n"
1954
- f'okstra lead contract (`{lead_contract_path}` → "Phase 3 — Teammate setup").\n'
1858
+ "## Single-Lead Phase (no worker dispatch)\n"
1955
1859
  "\n"
1956
- "Claude Code v2.1.178 removed the `TeamCreate` / `TeamDelete` tools and the\n"
1957
- "`Agent(...)` `team_name` parameter. With `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`\n"
1958
- "(seeded by `okstra install`) this session already owns one implicit team;\n"
1959
- "workers spawned with `Agent(name: ...)` auto-join it. Do NOT probe or call\n"
1960
- "`TeamCreate` — its absence is the normal state on every current Claude Code,\n"
1961
- "not an 'Agent Teams unavailable' signal, and never a reason to degrade the run.\n"
1962
- "\n"
1963
- "Required actions, in order, regardless of how many workers are selected\n"
1964
- "for this run (roster comes from `resultContract.requiredWorkerRoles` in\n"
1965
- "`task-manifest.json` — it may be 1, 2, 3, or more workers):\n"
1860
+ "This run has no worker roster or convergence loop. The lead performs "
1861
+ "the phase inline and follows the selected adapter only for user prompts, "
1862
+ "artifact operations, usage collection, and cleanup."
1863
+ )
1864
+ else:
1865
+ team_creation_gate_block = (
1866
+ "## Runtime Adapter Dispatch Gate (BLOCKING)\n"
1966
1867
  "\n"
1967
- f"1. Read the team-contract resource (`{team_contract_path}`) and verify the selected worker\n"
1968
- " roster against `task-manifest.json`'s `resultContract.requiredWorkerRoles`.\n"
1969
- "2. Record team-state BEFORE any dispatch:\n"
1970
- f' `teamName: "{team_name}"` (audit/display label only) AND\n'
1971
- ' `teamCreate: { attempted: false, status: "implicit",'
1972
- " splitPane: <true if $TMUX set, else false> }`.\n"
1973
- '3. Dispatch every worker with `Agent(name: "<role>-worker",'
1974
- " run_in_background: true)` and NO `team_name`. Split-pane teammates appear\n"
1975
- " automatically when `$TMUX` is set (`teammateMode: auto`); when it is\n"
1976
- " unset they run in-process — both are normal, not a degraded run."
1868
+ f"- Selected lead runtime: `{lead_runtime}`\n"
1869
+ f"- Selected adapter contract: `{adapter_contract_path}`\n"
1870
+ "- Read that adapter before Phase 3. Execute `prompt_user`, "
1871
+ "`dispatch_worker`, `await_workers`, `redispatch_worker`, "
1872
+ "`shutdown_workers`, `record_lead_event`, and `collect_usage` only "
1873
+ "through its mapping.\n"
1874
+ "- Do not call a primitive documented by an unselected adapter.\n"
1875
+ "- Record the adapter-required Phase 3 state and emit the canonical "
1876
+ "`PROGRESS: phase-3-team-create ...` checkpoint before dispatch.\n"
1877
+ f"{adapter_setup_block}\n"
1878
+ "- The run manifest is authoritative for concurrent-run metadata, "
1879
+ "dispatch backend, and artifact paths."
1977
1880
  )
1978
1881
 
1882
+ lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
1883
+ lead_bootstrap_instruction = (
1884
+ f"Read the Lifecycle core contract at `{lead_contract_path}`, then read the "
1885
+ f"selected runtime adapter at `{adapter_contract_path}`. Read the manifests "
1886
+ "below for all task metadata, paths, model assignments, and worker roster. "
1887
+ "Execute `write_artifact`, `dispatch_worker`, and `await_workers` only through "
1888
+ "the selected adapter's mapping. Follow the lifecycle core's lazy-read rules "
1889
+ "for support resources; do not invoke hidden/internal skills."
1890
+ )
1891
+ worker_preamble_path = ctx.get(
1892
+ "WORKER_PROMPT_PREAMBLE_PATH",
1893
+ str(okstra_home() / "templates" / "worker-prompt-preamble.md"),
1894
+ )
1895
+ worker_dispatch_guidance = (
1896
+ "## Worker Dispatch Contract\n"
1897
+ "\n"
1898
+ "- Worker prompt paths in `task-manifest.json` are assigned prompt-history "
1899
+ "locations, not pre-rendered completion markers. An absent file means its "
1900
+ "dispatch history has not been materialized yet.\n"
1901
+ "- Construct each required worker prompt and follow the lifecycle core plus "
1902
+ "selected adapter to persist its assigned history through `write_artifact` "
1903
+ "and start it through `dispatch_worker`. Existing-history handling must stay "
1904
+ "idempotent under those contracts.\n"
1905
+ "- Use `await_workers` through the selected adapter and confirm terminal state "
1906
+ "plus every required Result Path. File presence alone is never a signal to "
1907
+ "skip; worker selection and skipped status come from `team-state`.\n"
1908
+ "- Every worker prompt must include the anchor header "
1909
+ f"`**Worker Preamble Path:** {worker_preamble_path}`. That file is the canonical "
1910
+ "source for Required Reading, Error Reporting, and Output sections; do not "
1911
+ "re-inline those blocks into worker prompt bodies."
1912
+ )
1913
+
1979
1914
  if lead_runtime == "external":
1980
- lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
1981
- lead_bootstrap_instruction = (
1982
- "Read the manifests below for all task metadata, paths, model "
1983
- "assignments, and worker roster. Use the tmux worker dispatch gate "
1984
- "in this prompt; do not invoke Claude Code skills."
1985
- )
1986
1915
  lead_session_block = (
1987
1916
  "## Session\n"
1988
1917
  "\n"
1989
1918
  "- Lead runtime: `external`\n"
1990
1919
  "- Lead session accounting: artifact-only; this harness does not expose Claude Code jsonl session IDs."
1991
1920
  )
1992
- worker_dispatch_guidance = (
1993
- "## Tmux Worker Dispatch\n"
1994
- "\n"
1995
- "- Worker prompt paths in `task-manifest.json` are assigned history "
1996
- "locations. `okstra team dispatch` materializes missing prompt files "
1997
- "from the run manifest and active-run-context; existing prompt files "
1998
- "are never overwritten.\n"
1999
- "- Do not call `TeamCreate`, `Agent(...)`, or `okstra codex-dispatch`. "
2000
- "Use `okstra team dispatch` and `okstra team await` from the gate "
2001
- "above for worker execution.\n"
2002
- "- File presence is not a signal to skip. Worker selection and skipped "
2003
- "statuses come from `team-state`, never from the prompts directory."
2004
- )
2005
1921
  elif lead_runtime == "codex":
2006
- lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
2007
- lead_bootstrap_instruction = (
2008
- "Read the manifests below for all task metadata, paths, model "
2009
- "assignments, and worker roster. Use the Codex dispatch gate in this "
2010
- "prompt; do not invoke Claude Code skills."
2011
- )
2012
1922
  lead_session_block = (
2013
1923
  "## Session\n"
2014
1924
  "\n"
@@ -2016,41 +1926,13 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2016
1926
  "- Codex run resume is artifact/checkpoint based; `okstra codex-run` "
2017
1927
  "does not create a Claude session id."
2018
1928
  )
2019
- worker_dispatch_guidance = (
2020
- "## Codex Worker Dispatch\n"
2021
- "\n"
2022
- "- Worker prompt paths in `task-manifest.json` are assigned history "
2023
- "locations. `okstra codex-dispatch` materializes missing prompt files "
2024
- "from the run manifest and active-run-context; existing prompt files "
2025
- "are never overwritten.\n"
2026
- "- Do not call `TeamCreate` or `Agent(...)`. Use the command in the "
2027
- "Codex Dispatch Gate above for worker execution.\n"
2028
- "- File presence is not a signal to skip. Worker selection and skipped "
2029
- "statuses come from `team-state`, never from the prompts directory."
2030
- )
2031
1929
  else:
2032
- lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
2033
- lead_bootstrap_instruction = (
2034
- f"Read the Okstra lead contract at `{lead_contract_path}` now, then read "
2035
- "the manifests below for all task metadata, paths, model assignments, and "
2036
- "worker roster. Follow the contract's lazy-read rules for support resources; "
2037
- "do not invoke hidden/internal skills."
2038
- )
2039
1930
  lead_session_block = (
2040
1931
  "## Session\n"
2041
1932
  "\n"
2042
1933
  f"- Session ID: `{ctx.get('CLAUDE_SESSION_ID', '')}`\n"
2043
1934
  f"- Resume: `{ctx.get('CLAUDE_RESUME_COMMAND_RELATIVE_PATH', '')}`"
2044
1935
  )
2045
- worker_dispatch_guidance = (
2046
- "## Worker Prompt Files (not pre-rendered)\n"
2047
- "\n"
2048
- "- The `runs/<phase>/prompts/<worker>-worker-prompt-*.md` files referenced in `task-manifest.json → artifacts.workerPromptPathByWorkerId` are **NOT** rendered by the okstra runtime. Those paths are the *assigned* locations where the prompt history MUST be written at dispatch time.\n"
2049
- "- Therefore: at the start of every phase the prompts/ directory is normally empty (or contains only previously-dispatched workers' files). This is expected. Do NOT narrate it as \"missing\", \"누락\", or \"not yet rendered\" — it just means dispatch has not happened yet.\n"
2050
- "- Before dispatching any required worker, **you (the lead) construct the worker prompt and persist it to the assigned absolute path using `Write`** (per `team-contract` rule 6). Only after persisting do you call the worker subagent (Agent tool / Codex / Antigravity wrapper). The wrapper subagents will also re-write the same file on their end; the double-write is intentional and idempotent.\n"
2051
- "- Do not \"check if the file exists and skip dispatch\" — file presence is not a signal to skip. Worker selection and skipping rules come from team-state, never from prompts/ directory contents.\n"
2052
- f"- **Worker Preamble Path (single anchor — replaces inlined `[Required reading]` and `[Error reporting]` blocks).** Every worker prompt you construct MUST include the anchor header `**Worker Preamble Path:** {ctx.get('WORKER_PROMPT_PREAMBLE_PATH', str(okstra_home() / 'templates' / 'worker-prompt-preamble.md'))}`. The file at that path is the canonical SSOT for Required Reading + Error Reporting + Output sections; workers Read it end-to-end before producing output. Do NOT re-inline those blocks into worker prompt bodies — that is the legacy ~80-line dispatch boilerplate that this anchor is designed to replace."
2053
- )
2054
1936
 
2055
1937
  # Compute results (deterministic from ctx, 덮어쓰기)
2056
1938
  ctx["LEAD_INTRO_LINE"] = lead_intro_line
@@ -597,24 +597,32 @@ _ENUM_CUE_WORDS: tuple[str, ...] = (
597
597
 
598
598
  def _parse_enum_options(statement: str) -> list[tuple[str, str]]:
599
599
  """Return ``[(letter, text)]`` pairs if ``statement`` looks like a
600
- ``(a) X (b) Y (c) Z`` enumeration, else ``[]``. Stops at the first
601
- recommendation cue (``권장``/``추천``/``사유``/``근거``) so the
600
+ ``(a) X (b) Y (c) Z`` enumeration, else ``[]``. The sequence may start
601
+ at any letter — a statement enum starts at ``(a)``, while the
602
+ ``Alternatives`` cell of the Expected-form contract starts at ``(b)``
603
+ (the recommended answer is ``(a)``) — so parsing keys off the first
604
+ ``(letter)`` marker found and walks the sequence from there. Stops at
605
+ the first recommendation cue (``권장``/``추천``/``사유``/``근거``) so the
602
606
  "(c) — recommended because ..." tail doesn't leak into the last
603
607
  option's text. Requires ≥ 2 options to trigger select rendering.
604
608
  """
605
609
  if not statement:
606
610
  return []
611
+ first = re.search(r"\(([a-z])\) ", statement)
612
+ if not first:
613
+ return []
614
+ letters = [chr(c) for c in range(ord(first.group(1)), ord("z") + 1)]
607
615
  out: list[tuple[str, str]] = []
608
616
  cursor = 0
609
- for i, letter in enumerate(_ENUM_LETTERS):
617
+ for i, letter in enumerate(letters):
610
618
  anchor = f"({letter}) "
611
619
  idx = statement.find(anchor, cursor)
612
620
  if idx < 0:
613
621
  break
614
622
  start = idx + len(anchor)
615
623
  end_candidates: list[int] = []
616
- if i + 1 < len(_ENUM_LETTERS):
617
- n_idx = statement.find(f"({_ENUM_LETTERS[i + 1]}) ", start)
624
+ if i + 1 < len(letters):
625
+ n_idx = statement.find(f"({letters[i + 1]}) ", start)
618
626
  if n_idx >= 0:
619
627
  end_candidates.append(n_idx)
620
628
  for cue in _ENUM_CUE_WORDS:
@@ -643,6 +651,13 @@ def _lettered_option(letter: str, text: str) -> str:
643
651
  return f"({letter}) {text}"
644
652
 
645
653
 
654
+ def _strip_leading_letter_label(text: str) -> str:
655
+ """Drop a leading ``(a)`` / ``(b)`` … contract label so the caller can
656
+ re-letter the option exactly once (the contract labels the recommended
657
+ answer `(a)` and alternatives from `(b)`)."""
658
+ return re.sub(r"^\([a-z]\)\s*", "", text)
659
+
660
+
646
661
  def _parse_expected_form_options(expected_form: str) -> list[tuple[str, str]]:
647
662
  """Parse the ``Expected form`` contract format
648
663
  (``Recommended: <answer> — <rationale>; Alternatives: <options>``,
@@ -662,7 +677,7 @@ def _parse_expected_form_options(expected_form: str) -> list[tuple[str, str]]:
662
677
  alt_body = rec_body[alt_idx + len(_ALTERNATIVES_CUE):]
663
678
  rec_body = rec_body[:alt_idx]
664
679
  # The rationale follows " — "; the option keeps only the answer part.
665
- answer = rec_body.split(" — ", 1)[0].strip(" .,;—-")
680
+ answer = _strip_leading_letter_label(rec_body.split(" — ", 1)[0].strip(" .,;—-"))
666
681
  options: list[tuple[str, str]] = []
667
682
  next_letter_idx = 0
668
683
  if answer:
@@ -678,7 +693,7 @@ def _parse_expected_form_options(expected_form: str) -> list[tuple[str, str]]:
678
693
  options.append((letter, _lettered_option(letter, text)))
679
694
  next_letter_idx += 1
680
695
  else:
681
- alt_text = alt_body.strip(" .,;—-")
696
+ alt_text = _strip_leading_letter_label(alt_body.strip(" .,;—-"))
682
697
  if alt_text and next_letter_idx < len(_ENUM_LETTERS):
683
698
  letter = _ENUM_LETTERS[next_letter_idx]
684
699
  options.append((letter, _lettered_option(letter, alt_text)))
@@ -15,7 +15,9 @@ recorded pane — those are left for graceful shutdown.
15
15
  CC v2.1.178 removed ``TeamDelete``: the implicit team ends with the session and
16
16
  run-end teardown only dismisses teammates via ``shutdown_request``. Reconcile
17
17
  keeps the roster honest so a dismissed-but-dead member does not linger as a
18
- live pill — see okstra-lead-contract.md "Run-scoped pane & teammate lifecycle".
18
+ live pill — see okstra-lead-contract.md "Run-scoped worker-resource lifecycle"
19
+ for neutral cleanup ownership and the selected Claude adapter's
20
+ "Run-scoped resource lifecycle" for this provider-specific reconciliation.
19
21
 
20
22
  Roster resolution (``--project-root``) exists because ``team-state.lead.sessionId``
21
23
  is a phase-3 snapshot: Claude Code re-issues the session id across resume /
@@ -1508,6 +1508,7 @@
1508
1508
  "aborted-non-result"
1509
1509
  ]
1510
1510
  },
1511
+ "selfFixRoundApplied": { "type": "boolean" },
1511
1512
  "planItems": {
1512
1513
  "type": "array",
1513
1514
  "items": {
@@ -1525,6 +1526,7 @@
1525
1526
  { "type": "string", "pattern": "^C-\\d{3,}$" }
1526
1527
  ]
1527
1528
  },
1529
+ "selfFixNote": { "type": "string" },
1528
1530
  "verdicts": {
1529
1531
  "type": "array",
1530
1532
  "items": {
@@ -1537,6 +1539,7 @@
1537
1539
  "enum": ["AGREE", "DISAGREE", "SUPPLEMENT", "verification-error"]
1538
1540
  },
1539
1541
  "breakageKind": { "type": "string" },
1542
+ "fixability": { "enum": ["planner-fixable", "needs-user-input"] },
1540
1543
  "note": { "type": "string" }
1541
1544
  }
1542
1545
  }