okstra 0.121.1 → 0.123.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 (67) hide show
  1. package/README.md +4 -2
  2. package/docs/architecture/storage-model.md +14 -0
  3. package/docs/architecture.md +38 -8
  4. package/docs/cli.md +45 -5
  5. package/docs/for-ai/README.md +2 -2
  6. package/docs/for-ai/skills/okstra-rollup.md +1 -1
  7. package/docs/for-ai/skills/{okstra-schedule.md → okstra-schedule-gen.md} +3 -3
  8. package/docs/project-structure-overview.md +4 -3
  9. package/docs/task-process/implementation-planning.md +33 -9
  10. package/docs/task-process/implementation.md +21 -2
  11. package/package.json +1 -1
  12. package/runtime/BUILD.json +2 -2
  13. package/runtime/agents/workers/claude-worker.md +1 -1
  14. package/runtime/bin/lib/okstra/usage.sh +3 -3
  15. package/runtime/prompts/launch.template.md +9 -6
  16. package/runtime/prompts/lead/adapters/claude-code.md +104 -0
  17. package/runtime/prompts/lead/adapters/codex.md +45 -0
  18. package/runtime/prompts/lead/adapters/external.md +46 -0
  19. package/runtime/prompts/lead/context-loader.md +5 -5
  20. package/runtime/prompts/lead/convergence.md +11 -28
  21. package/runtime/prompts/lead/okstra-lead-contract.md +44 -94
  22. package/runtime/prompts/lead/plan-body-verification.md +50 -5
  23. package/runtime/prompts/lead/report-writer.md +59 -54
  24. package/runtime/prompts/lead/team-contract.md +33 -106
  25. package/runtime/prompts/profiles/_common-contract.md +3 -3
  26. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -1
  27. package/runtime/prompts/profiles/_implementation-executor.md +6 -1
  28. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  29. package/runtime/prompts/profiles/final-verification.md +2 -0
  30. package/runtime/prompts/profiles/implementation-planning.md +14 -4
  31. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  32. package/runtime/prompts/wizard/prompts.ko.json +44 -0
  33. package/runtime/python/okstra_ctl/codex_dispatch.py +23 -1
  34. package/runtime/python/okstra_ctl/design_prep.py +1462 -0
  35. package/runtime/python/okstra_ctl/design_surfaces.py +243 -0
  36. package/runtime/python/okstra_ctl/final_report_schema.py +33 -1
  37. package/runtime/python/okstra_ctl/implementation_stage.py +35 -0
  38. package/runtime/python/okstra_ctl/incremental_carry.py +294 -21
  39. package/runtime/python/okstra_ctl/incremental_scope.py +51 -5
  40. package/runtime/python/okstra_ctl/lead_runtime.py +4 -0
  41. package/runtime/python/okstra_ctl/material.py +1 -1
  42. package/runtime/python/okstra_ctl/model_discovery.py +98 -0
  43. package/runtime/python/okstra_ctl/models.py +8 -3
  44. package/runtime/python/okstra_ctl/render.py +81 -194
  45. package/runtime/python/okstra_ctl/report_views.py +22 -7
  46. package/runtime/python/okstra_ctl/run.py +53 -5
  47. package/runtime/python/okstra_ctl/team_reconcile.py +3 -1
  48. package/runtime/python/okstra_ctl/user_response.py +67 -2
  49. package/runtime/python/okstra_ctl/wizard.py +283 -3
  50. package/runtime/python/okstra_token_usage/report.py +11 -0
  51. package/runtime/schemas/final-report-v1.0.schema.json +339 -0
  52. package/runtime/skills/okstra-inspect/SKILL.md +2 -2
  53. package/runtime/skills/okstra-rollup/SKILL.md +1 -1
  54. package/runtime/skills/{okstra-schedule → okstra-schedule-gen}/SKILL.md +62 -20
  55. package/runtime/skills/okstra-user-response/SKILL.md +8 -6
  56. package/runtime/templates/reports/final-report.template.md +71 -4
  57. package/runtime/templates/reports/i18n/en.json +32 -1
  58. package/runtime/templates/reports/i18n/ko.json +32 -1
  59. package/runtime/validators/validate-run.py +515 -5
  60. package/runtime/validators/validate-schedule.py +11 -7
  61. package/runtime/validators/validate_session_conformance.py +58 -33
  62. package/src/cli-registry.mjs +14 -0
  63. package/src/commands/inspect/design-prep.mjs +23 -0
  64. package/src/commands/inspect/stage-map.mjs +100 -0
  65. package/src/commands/lifecycle/install.mjs +3 -0
  66. package/src/commands/lifecycle/uninstall.mjs +8 -23
  67. package/src/lib/skill-catalog.mjs +10 -1
@@ -1351,6 +1351,11 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1351
1351
  }
1352
1352
  if ctx.get("FIX_CYCLE_ID"):
1353
1353
  payload["fixCycleId"] = ctx["FIX_CYCLE_ID"]
1354
+ payload["reportContracts"] = (
1355
+ ["implementation-design-prep-v1"]
1356
+ if ctx.get("TASK_TYPE") == "implementation-planning"
1357
+ else []
1358
+ )
1354
1359
  _write_json(Path(run_manifest_path), payload)
1355
1360
 
1356
1361
 
@@ -1735,6 +1740,9 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1735
1740
  lead_contract_path = ctx.get("OKSTRA_LEAD_CONTRACT_PATH") or str(
1736
1741
  home_prompts / "lead" / "okstra-lead-contract.md"
1737
1742
  )
1743
+ adapter_contract_path = ctx.get("OKSTRA_LEAD_ADAPTER_CONTRACT_PATH") or str(
1744
+ home_prompts / _lead_info(ctx).adapter_contract_relative_path
1745
+ )
1738
1746
  context_loader_path = ctx.get("OKSTRA_CONTEXT_LOADER_PATH") or str(
1739
1747
  home_prompts / "lead" / "context-loader.md"
1740
1748
  )
@@ -1756,7 +1764,10 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1756
1764
  okstra_runtime_resources_block = (
1757
1765
  "## Okstra Runtime Resources\n"
1758
1766
  "\n"
1759
- f"- Lead contract: `{lead_contract_path}`\n"
1767
+ f"- Lifecycle core contract: `{lead_contract_path}`\n"
1768
+ f"- Selected runtime adapter contract: `{adapter_contract_path}`\n"
1769
+ "- Read the lifecycle core first, then the selected adapter. "
1770
+ "Only the selected adapter may define host tool spelling.\n"
1760
1771
  f"- Context loader: `{context_loader_path}`\n"
1761
1772
  f"- Team contract: `{team_contract_path}`\n"
1762
1773
  f"- Convergence contract: `{convergence_path}`\n"
@@ -1822,193 +1833,97 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
1822
1833
  else "- `Antigravity worker` is not selected for this run, so no Antigravity attempt is required."
1823
1834
  )
1824
1835
 
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
1836
  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:
1837
+ adapter_setup_facts: list[str] = []
1838
+ if lead_runtime == "claude-code":
1935
1839
  base_name = f'okstra-{ctx.get("TASK_KEY", "")}'
1936
1840
  impl_stage = str(ctx.get("EFFECTIVE_STAGES", "") or "").strip()
1937
1841
  fv_stage = str(ctx.get("RUN_STAGE", "") or "").strip()
1938
1842
  stage_suffix = ""
1939
1843
  if task_type == "implementation" and impl_stage:
1940
- # stage 격리 run 은 stage 별 team — 같은 task 의 다른 stage 가 남긴
1941
- # team 과 이름이 충돌하지 않는다(worktree branch `-s<N>` 접미사와 동형).
1942
1844
  stage_suffix = f"-s{impl_stage}"
1943
1845
  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
1846
  stage_suffix = f"-fv-s{fv_stage}"
1949
- team_name = sanitize_team_name(base_name, stage_suffix)
1847
+ adapter_setup_facts.append(
1848
+ f"- Adapter audit label: `{sanitize_team_name(base_name, stage_suffix)}`"
1849
+ )
1850
+ concurrent_stages = str(ctx.get("CONCURRENT_RUN_STAGES", "") or "").strip()
1851
+ if concurrent_stages:
1852
+ adapter_setup_facts.append(
1853
+ f"- Prepare-recorded concurrent stages: `{concurrent_stages}`"
1854
+ )
1855
+ adapter_setup_block = (
1856
+ "\n".join(adapter_setup_facts)
1857
+ if adapter_setup_facts
1858
+ else "- Adapter setup facts: none"
1859
+ )
1860
+
1861
+ if task_type == "release-handoff" or not selected:
1950
1862
  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'
1863
+ "## Single-Lead Phase (no worker dispatch)\n"
1955
1864
  "\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"
1865
+ "This run has no worker roster or convergence loop. The lead performs "
1866
+ "the phase inline and follows the selected adapter only for user prompts, "
1867
+ "artifact operations, usage collection, and cleanup."
1868
+ )
1869
+ else:
1870
+ team_creation_gate_block = (
1871
+ "## Runtime Adapter Dispatch Gate (BLOCKING)\n"
1966
1872
  "\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."
1873
+ f"- Selected lead runtime: `{lead_runtime}`\n"
1874
+ f"- Selected adapter contract: `{adapter_contract_path}`\n"
1875
+ "- Read that adapter before Phase 3. Execute `prompt_user`, "
1876
+ "`dispatch_worker`, `await_workers`, `redispatch_worker`, "
1877
+ "`shutdown_workers`, `record_lead_event`, and `collect_usage` only "
1878
+ "through its mapping.\n"
1879
+ "- Do not call a primitive documented by an unselected adapter.\n"
1880
+ "- Record the adapter-required Phase 3 state and emit the canonical "
1881
+ "`PROGRESS: phase-3-team-create ...` checkpoint before dispatch.\n"
1882
+ f"{adapter_setup_block}\n"
1883
+ "- The run manifest is authoritative for concurrent-run metadata, "
1884
+ "dispatch backend, and artifact paths."
1977
1885
  )
1978
1886
 
1887
+ lead_intro_line = f"You are `{lead_role}` for project `{ctx.get('PROJECT_ID', '')}`."
1888
+ lead_bootstrap_instruction = (
1889
+ f"Read the Lifecycle core contract at `{lead_contract_path}`, then read the "
1890
+ f"selected runtime adapter at `{adapter_contract_path}`. Read the manifests "
1891
+ "below for all task metadata, paths, model assignments, and worker roster. "
1892
+ "Execute `write_artifact`, `dispatch_worker`, and `await_workers` only through "
1893
+ "the selected adapter's mapping. Follow the lifecycle core's lazy-read rules "
1894
+ "for support resources; do not invoke hidden/internal skills."
1895
+ )
1896
+ worker_preamble_path = ctx.get(
1897
+ "WORKER_PROMPT_PREAMBLE_PATH",
1898
+ str(okstra_home() / "templates" / "worker-prompt-preamble.md"),
1899
+ )
1900
+ worker_dispatch_guidance = (
1901
+ "## Worker Dispatch Contract\n"
1902
+ "\n"
1903
+ "- Worker prompt paths in `task-manifest.json` are assigned prompt-history "
1904
+ "locations, not pre-rendered completion markers. An absent file means its "
1905
+ "dispatch history has not been materialized yet.\n"
1906
+ "- Construct each required worker prompt and follow the lifecycle core plus "
1907
+ "selected adapter to persist its assigned history through `write_artifact` "
1908
+ "and start it through `dispatch_worker`. Existing-history handling must stay "
1909
+ "idempotent under those contracts.\n"
1910
+ "- Use `await_workers` through the selected adapter and confirm terminal state "
1911
+ "plus every required Result Path. File presence alone is never a signal to "
1912
+ "skip; worker selection and skipped status come from `team-state`.\n"
1913
+ "- Every worker prompt must include the anchor header "
1914
+ f"`**Worker Preamble Path:** {worker_preamble_path}`. That file is the canonical "
1915
+ "source for Required Reading, Error Reporting, and Output sections; do not "
1916
+ "re-inline those blocks into worker prompt bodies."
1917
+ )
1918
+
1979
1919
  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
1920
  lead_session_block = (
1987
1921
  "## Session\n"
1988
1922
  "\n"
1989
1923
  "- Lead runtime: `external`\n"
1990
1924
  "- Lead session accounting: artifact-only; this harness does not expose Claude Code jsonl session IDs."
1991
1925
  )
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
1926
  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
1927
  lead_session_block = (
2013
1928
  "## Session\n"
2014
1929
  "\n"
@@ -2016,41 +1931,13 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2016
1931
  "- Codex run resume is artifact/checkpoint based; `okstra codex-run` "
2017
1932
  "does not create a Claude session id."
2018
1933
  )
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
1934
  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
1935
  lead_session_block = (
2040
1936
  "## Session\n"
2041
1937
  "\n"
2042
1938
  f"- Session ID: `{ctx.get('CLAUDE_SESSION_ID', '')}`\n"
2043
1939
  f"- Resume: `{ctx.get('CLAUDE_RESUME_COMMAND_RELATIVE_PATH', '')}`"
2044
1940
  )
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
1941
 
2055
1942
  # Compute results (deterministic from ctx, 덮어쓰기)
2056
1943
  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)))
@@ -46,6 +46,7 @@ from .final_report_schema import load_schema
46
46
  from .final_report_paths import final_report_data_path as _final_report_data_path
47
47
  from .lead_events import LeadEvent, append_lead_event
48
48
  from .lead_runtime import ALLOWED_LEAD_RUNTIMES
49
+ from .model_discovery import normalize_execution_for_dispatch
49
50
  from .models import ModelAssignment, default_model, resolve_model_metadata
50
51
  from .schema_excerpt import build_schema_excerpt
51
52
  from .path_resolve import relative_to_project_root, resolve_user_file
@@ -1412,6 +1413,27 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
1412
1413
  )
1413
1414
 
1414
1415
 
1416
+ def _normalized_execution(meta: ModelAssignment, provider: str, role: str) -> str:
1417
+ """Route a role's catalog execution value through the CLI-identity normalizer
1418
+ so the manifest carries the dispatch-correct spelling. SSOT for the mapping
1419
+ lives in model_discovery; this is only the run.py call seam."""
1420
+ return normalize_execution_for_dispatch(
1421
+ provider=provider, execution=meta.execution, display=meta.display, role=role,
1422
+ )
1423
+
1424
+
1425
+ def _worker_execution_value(
1426
+ meta: ModelAssignment, provider: str, role: str, workers: list[str],
1427
+ ) -> str:
1428
+ """Normalize a worker role's execution value only when that provider's
1429
+ worker is in the resolved roster; otherwise return the raw catalog value.
1430
+ A worker that will not be dispatched must not probe its CLI or hard-fail
1431
+ the whole prep (the CLI-identity check belongs only to workers that run)."""
1432
+ if provider in workers:
1433
+ return _normalized_execution(meta, provider=provider, role=role)
1434
+ return meta.execution
1435
+
1436
+
1415
1437
  StageRunClaim = _implementation_stage.StageRunClaim
1416
1438
 
1417
1439
 
@@ -1577,7 +1599,9 @@ def _write_instruction_set_sources(
1577
1599
  instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
1578
1600
  instruction_set.mkdir(parents=True, exist_ok=True)
1579
1601
  profile_rendered = profile_content
1580
- for key in (
1602
+ if inp.task_type == "implementation":
1603
+ profile_rendered += "\n\n{{DESIGN_PREP_CONTEXT}}"
1604
+ profile_tokens = (
1581
1605
  "EXECUTOR_PROVIDER",
1582
1606
  "EXECUTOR_DISPLAY_NAME",
1583
1607
  "EXECUTOR_WORKER_AGENT",
@@ -1588,10 +1612,28 @@ def _write_instruction_set_sources(
1588
1612
  "EXECUTOR_WORKTREE_BASE_REF",
1589
1613
  "EXECUTOR_WORKTREE_STATUS",
1590
1614
  "EXECUTOR_WORKTREE_NOTE",
1615
+ "DESIGN_PREP_CONTEXT",
1591
1616
  "PHASE_FORBIDDEN_ACTIONS",
1592
- ):
1617
+ )
1618
+ for key in profile_tokens:
1593
1619
  profile_rendered = profile_rendered.replace("{{" + key + "}}", ctx.get(key, ""))
1594
1620
  (instruction_set / "analysis-profile.md").write_text(profile_rendered, encoding="utf-8")
1621
+ if inp.task_type == "implementation":
1622
+ executor_source = (
1623
+ Path(ctx["WORKSPACE_ROOT"])
1624
+ / "prompts"
1625
+ / "profiles"
1626
+ / "_implementation-executor.md"
1627
+ )
1628
+ executor_rendered = executor_source.read_text(encoding="utf-8")
1629
+ for key in profile_tokens:
1630
+ executor_rendered = executor_rendered.replace(
1631
+ "{{" + key + "}}", ctx.get(key, "")
1632
+ )
1633
+ (instruction_set / "implementation-executor.md").write_text(
1634
+ executor_rendered,
1635
+ encoding="utf-8",
1636
+ )
1595
1637
  (instruction_set / "analysis-material.md").write_text(review_material, encoding="utf-8")
1596
1638
  shutil.copyfile(inp.brief_path, instruction_set / "task-brief.md")
1597
1639
  if inp.clarification_response_path:
@@ -2058,16 +2100,22 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
2058
2100
  "CLAUDE_WORKER_MODEL": cw.display,
2059
2101
  "CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
2060
2102
  "CODEX_WORKER_MODEL": co.display,
2061
- "CODEX_WORKER_MODEL_EXECUTION_VALUE": co.execution,
2103
+ "CODEX_WORKER_MODEL_EXECUTION_VALUE": _worker_execution_value(
2104
+ co, "codex", "worker", workers,
2105
+ ),
2062
2106
  "ANTIGRAVITY_WORKER_MODEL": ge.display,
2063
- "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": ge.execution,
2107
+ "ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": _worker_execution_value(
2108
+ ge, "antigravity", "worker", workers,
2109
+ ),
2064
2110
  "REPORT_WRITER_MODEL": rw.display,
2065
2111
  "REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
2066
2112
  "EXECUTOR_PROVIDER": executor_provider,
2067
2113
  "EXECUTOR_DISPLAY_NAME": executor_display_name,
2068
2114
  "EXECUTOR_WORKER_AGENT": executor_worker_agent,
2069
2115
  "EXECUTOR_MODEL_DISPLAY": executor_model_meta.display,
2070
- "EXECUTOR_MODEL_EXECUTION_VALUE": executor_model_meta.execution,
2116
+ "EXECUTOR_MODEL_EXECUTION_VALUE": _normalized_execution(
2117
+ executor_model_meta, provider=executor_provider, role="executor",
2118
+ ),
2071
2119
  "CRITIC_CHOICE": critic_choice,
2072
2120
  "RELATED_TASKS_JSON": related_tasks_json_str,
2073
2121
  "RELATED_TASKS_BULLETS": bullets,
@@ -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 /
@@ -26,6 +26,7 @@ from okstra_ctl.clarification_items import (
26
26
  parse_clarification_rows,
27
27
  scan_approval_gate,
28
28
  section_1_present_but_unparsed,
29
+ _section_1_slice,
29
30
  )
30
31
 
31
32
  _APPROVAL_HEADING_RE = re.compile(r"^## APPROVAL\s*$", re.MULTILINE)
@@ -146,8 +147,14 @@ def list_awaiting_tasks(home: Path, project_id: str, limit: int) -> list[dict]:
146
147
  return out[:limit] if limit > 0 else out
147
148
 
148
149
 
149
- _SECTION_REF_RE = re.compile(r"§[\d.]+|C-\d+|[\w./-]+\.\w+:\d+")
150
+ # ID tokens are the report body's own cross-reference labels (RB-002, FU-001,
151
+ # O-002, C-017 …) — always uppercase-led so `gpt-5` and lowercase slugs don't
152
+ # match. `§x.y` and `path.ext:line` are the other two ref shapes.
153
+ _SECTION_REF_RE = re.compile(r"§[\d.]+|[A-Z]{1,4}-\d+|[\w./-]+\.\w+:\d+")
154
+ _ID_TOKEN_RE = re.compile(r"^[A-Z]{1,4}-\d+$")
150
155
  _ALTERNATIVES_CUE = "Alternatives:"
156
+ _DEFINITION_SNIPPET_CAP = 200
157
+ _SNIPPET_NOISE_RE = re.compile(r'<a id="[^"]*"></a>|`|\*\*')
151
158
 
152
159
 
153
160
  def _alternatives_from_expected(expected_form: str) -> list[str]:
@@ -159,6 +166,63 @@ def _alternatives_from_expected(expected_form: str) -> list[str]:
159
166
  return [p.strip(" .,;—-") for p in parts if p.strip(" .,;—-")]
160
167
 
161
168
 
169
+ def _clean_snippet(line: str) -> str:
170
+ s = _SNIPPET_NOISE_RE.sub("", line).strip()
171
+ s = re.sub(r"^[-*+]\s+|^\d+\.\s+", "", s) # drop a leading list marker
172
+ s = re.sub(r"\s*\|\s*", " | ", s).strip(" |")
173
+ s = re.sub(r"\s+", " ", s)
174
+ if len(s) > _DEFINITION_SNIPPET_CAP:
175
+ s = s[:_DEFINITION_SNIPPET_CAP].rstrip() + "…"
176
+ return s
177
+
178
+
179
+ def _resolve_id_token(report_text: str, token: str, s1_slice: str | None) -> str | None:
180
+ """Find where an internal report token (RB-002, FU-001, …) is defined.
181
+
182
+ Prefer a body line where the token is followed by a `:`/`—`/`-`/`)`
183
+ separator (its definition), else the first body occurrence. §1 rows are
184
+ skipped so the clarification statement being explained is never returned
185
+ as its own definition."""
186
+ definition_re = re.compile(rf"{re.escape(token)}\**\s*[)\]]?\s*[:—–-]")
187
+ fallback: str | None = None
188
+ for line in report_text.splitlines():
189
+ if token not in line:
190
+ continue
191
+ stripped = line.strip()
192
+ if s1_slice is not None and stripped and stripped in s1_slice:
193
+ continue
194
+ if definition_re.search(stripped):
195
+ return _clean_snippet(stripped)
196
+ if fallback is None:
197
+ fallback = _clean_snippet(stripped)
198
+ return fallback
199
+
200
+
201
+ def _resolve_section_ref(report_text: str, ref: str) -> str | None:
202
+ num = ref.lstrip("§")
203
+ heading_re = re.compile(rf"^#{{1,6}}\s+{re.escape(num)}(?=[.\s]).*$", re.MULTILINE)
204
+ m = heading_re.search(report_text)
205
+ return _clean_snippet(m.group(0).lstrip("# ").strip()) if m else None
206
+
207
+
208
+ def resolve_refs(report_text: str, refs: list[str]) -> list[dict]:
209
+ """Resolve each context ref to its plain-text definition from the report
210
+ body, or None when the report text alone cannot resolve it (a `path:line`
211
+ pointer). The deterministic lookup the skill uses to rewrite each
212
+ clarification question in self-contained plain language."""
213
+ s1_slice = _section_1_slice(report_text)
214
+ resolved: list[dict] = []
215
+ for ref in refs:
216
+ if ref.startswith("§"):
217
+ definition = _resolve_section_ref(report_text, ref)
218
+ elif _ID_TOKEN_RE.match(ref):
219
+ definition = _resolve_id_token(report_text, ref, s1_slice)
220
+ else:
221
+ definition = None
222
+ resolved.append({"ref": ref, "definition": definition})
223
+ return resolved
224
+
225
+
162
226
  def show_open_rows(report_path: Path) -> dict:
163
227
  text = report_path.read_text(encoding="utf-8")
164
228
  rows = []
@@ -172,7 +236,8 @@ def show_open_rows(report_path: Path) -> dict:
172
236
  "status": it.status, "statement": statement,
173
237
  "recommended": expected, # raw cell keeps answer + rationale
174
238
  "alternatives": _alternatives_from_expected(expected),
175
- "contextRefs": refs})
239
+ "contextRefs": refs,
240
+ "resolvedRefs": resolve_refs(text, refs)})
176
241
  return {"reportPath": str(report_path), "rows": rows}
177
242
 
178
243