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.
- package/README.md +4 -2
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +38 -8
- package/docs/cli.md +45 -5
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-rollup.md +1 -1
- package/docs/for-ai/skills/{okstra-schedule.md → okstra-schedule-gen.md} +3 -3
- package/docs/project-structure-overview.md +4 -3
- package/docs/task-process/implementation-planning.md +33 -9
- package/docs/task-process/implementation.md +21 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/prompts/launch.template.md +9 -6
- package/runtime/prompts/lead/adapters/claude-code.md +104 -0
- package/runtime/prompts/lead/adapters/codex.md +45 -0
- package/runtime/prompts/lead/adapters/external.md +46 -0
- package/runtime/prompts/lead/context-loader.md +5 -5
- package/runtime/prompts/lead/convergence.md +11 -28
- package/runtime/prompts/lead/okstra-lead-contract.md +44 -94
- package/runtime/prompts/lead/plan-body-verification.md +50 -5
- package/runtime/prompts/lead/report-writer.md +59 -54
- package/runtime/prompts/lead/team-contract.md +33 -106
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +6 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/final-verification.md +2 -0
- package/runtime/prompts/profiles/implementation-planning.md +14 -4
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +44 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +23 -1
- package/runtime/python/okstra_ctl/design_prep.py +1462 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +243 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +33 -1
- package/runtime/python/okstra_ctl/implementation_stage.py +35 -0
- package/runtime/python/okstra_ctl/incremental_carry.py +294 -21
- package/runtime/python/okstra_ctl/incremental_scope.py +51 -5
- package/runtime/python/okstra_ctl/lead_runtime.py +4 -0
- package/runtime/python/okstra_ctl/material.py +1 -1
- package/runtime/python/okstra_ctl/model_discovery.py +98 -0
- package/runtime/python/okstra_ctl/models.py +8 -3
- package/runtime/python/okstra_ctl/render.py +81 -194
- package/runtime/python/okstra_ctl/report_views.py +22 -7
- package/runtime/python/okstra_ctl/run.py +53 -5
- package/runtime/python/okstra_ctl/team_reconcile.py +3 -1
- package/runtime/python/okstra_ctl/user_response.py +67 -2
- package/runtime/python/okstra_ctl/wizard.py +283 -3
- package/runtime/python/okstra_token_usage/report.py +11 -0
- package/runtime/schemas/final-report-v1.0.schema.json +339 -0
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/{okstra-schedule → okstra-schedule-gen}/SKILL.md +62 -20
- package/runtime/skills/okstra-user-response/SKILL.md +8 -6
- package/runtime/templates/reports/final-report.template.md +71 -4
- package/runtime/templates/reports/i18n/en.json +32 -1
- package/runtime/templates/reports/i18n/ko.json +32 -1
- package/runtime/validators/validate-run.py +515 -5
- package/runtime/validators/validate-schedule.py +11 -7
- package/runtime/validators/validate_session_conformance.py +58 -33
- package/src/cli-registry.mjs +14 -0
- package/src/commands/inspect/design-prep.mjs +23 -0
- package/src/commands/inspect/stage-map.mjs +100 -0
- package/src/commands/lifecycle/install.mjs +3 -0
- package/src/commands/lifecycle/uninstall.mjs +8 -23
- 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"-
|
|
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
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
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
|
-
"##
|
|
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
|
-
"
|
|
1957
|
-
"
|
|
1958
|
-
"
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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"
|
|
1968
|
-
"
|
|
1969
|
-
"
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
"
|
|
1973
|
-
|
|
1974
|
-
"
|
|
1975
|
-
"
|
|
1976
|
-
"
|
|
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 ``[]``.
|
|
601
|
-
|
|
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(
|
|
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(
|
|
617
|
-
n_idx = statement.find(f"({
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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
|
-
|
|
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
|
|