okstra 0.178.0 → 0.179.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 (110) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/execute/plan-verify.mjs +1 -1
  3. package/dist/commands/execute/worktree-status.mjs +8 -2
  4. package/dist/commands/execute/worktree-status.mjs.map +1 -1
  5. package/dist/commands/lifecycle/install.mjs +1 -1
  6. package/dist/commands/lifecycle/install.mjs.map +1 -1
  7. package/dist/commands/report/render-final-report.mjs +3 -3
  8. package/docs/architecture/storage-model.md +3 -3
  9. package/docs/architecture.md +10 -9
  10. package/docs/cli.md +11 -13
  11. package/docs/for-ai/skills/okstra-inspect.md +3 -3
  12. package/docs/for-ai/skills/okstra-schedule-gen.md +2 -2
  13. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  14. package/docs/project-structure-overview.md +10 -11
  15. package/docs/task-process/implementation-planning.md +1 -1
  16. package/docs/task-process/implementation.md +1 -1
  17. package/package.json +1 -1
  18. package/runtime/BUILD.json +2 -2
  19. package/runtime/agents/workers/report-writer-worker.md +11 -12
  20. package/runtime/bin/lib/okstra/globals.sh +2 -2
  21. package/runtime/bin/lib/okstra/interactive.sh +1 -1
  22. package/runtime/bin/lib/okstra/usage.sh +11 -9
  23. package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
  24. package/runtime/bin/okstra-central.sh +2 -2
  25. package/runtime/bin/okstra-render-final-report.py +1 -1
  26. package/runtime/bin/okstra-token-usage.py +1 -1
  27. package/runtime/prompts/launch.template.md +1 -1
  28. package/runtime/prompts/lead/adapters/cmux.md +6 -1
  29. package/runtime/prompts/lead/context-loader.md +3 -2
  30. package/runtime/prompts/lead/convergence.md +1 -1
  31. package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
  32. package/runtime/prompts/lead/plan-body-verification.md +3 -3
  33. package/runtime/prompts/lead/report-writer.md +21 -20
  34. package/runtime/prompts/lead/team-contract.md +1 -1
  35. package/runtime/prompts/profiles/_common-contract.md +5 -4
  36. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
  37. package/runtime/prompts/profiles/_implementation-executor.md +2 -1
  38. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  39. package/runtime/prompts/profiles/implementation-planning.md +9 -5
  40. package/runtime/prompts/profiles/implementation.md +4 -4
  41. package/runtime/prompts/profiles/improvement-discovery.md +2 -2
  42. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +5 -4
  43. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -4
  44. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +2 -2
  45. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +71 -9
  46. package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -4
  47. package/runtime/python/okstra_ctl/agent_prompt_cli.py +55 -3
  48. package/runtime/python/okstra_ctl/analysis_inputs.py +5 -3
  49. package/runtime/python/okstra_ctl/analysis_packet.py +21 -0
  50. package/runtime/python/okstra_ctl/backfill.py +12 -5
  51. package/runtime/python/okstra_ctl/consumers.py +70 -3
  52. package/runtime/python/okstra_ctl/convergence_engine.py +43 -17
  53. package/runtime/python/okstra_ctl/dispatch_core.py +47 -11
  54. package/runtime/python/okstra_ctl/dispatch_state.py +20 -19
  55. package/runtime/python/okstra_ctl/domain/worker_exec.py +13 -34
  56. package/runtime/python/okstra_ctl/domain/worker_presentation.py +128 -0
  57. package/runtime/python/okstra_ctl/execution_mutation_audit.py +5 -0
  58. package/runtime/python/okstra_ctl/final_report_paths.py +77 -1
  59. package/runtime/python/okstra_ctl/handoff.py +1 -2
  60. package/runtime/python/okstra_ctl/implementation_outcome.py +1 -1
  61. package/runtime/python/okstra_ctl/index.py +4 -4
  62. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +26 -12
  63. package/runtime/python/okstra_ctl/listing.py +4 -2
  64. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  65. package/runtime/python/okstra_ctl/manager_sync.py +1 -1
  66. package/runtime/python/okstra_ctl/path_hints.py +2 -2
  67. package/runtime/python/okstra_ctl/paths.py +13 -10
  68. package/runtime/python/okstra_ctl/plan_run_root.py +9 -5
  69. package/runtime/python/okstra_ctl/recap.py +3 -2
  70. package/runtime/python/okstra_ctl/reconcile.py +3 -1
  71. package/runtime/python/okstra_ctl/render.py +22 -22
  72. package/runtime/python/okstra_ctl/report_finalize.py +4 -4
  73. package/runtime/python/okstra_ctl/rollup.py +1 -1
  74. package/runtime/python/okstra_ctl/run.py +139 -284
  75. package/runtime/python/okstra_ctl/run_audit.py +5 -5
  76. package/runtime/python/okstra_ctl/run_index_row.py +2 -2
  77. package/runtime/python/okstra_ctl/session_transcript.py +89 -0
  78. package/runtime/python/okstra_ctl/stage_ledger.py +72 -0
  79. package/runtime/python/okstra_ctl/stage_map.py +28 -29
  80. package/runtime/python/okstra_ctl/stage_targets.py +61 -0
  81. package/runtime/python/okstra_ctl/user_response.py +97 -12
  82. package/runtime/python/okstra_ctl/wizard.py +43 -65
  83. package/runtime/python/okstra_ctl/worker_prompt_body.py +6 -7
  84. package/runtime/python/okstra_ctl/worker_runner.py +76 -213
  85. package/runtime/python/okstra_ctl/workflow.py +1 -1
  86. package/runtime/python/okstra_ctl/wrapper_status.py +23 -0
  87. package/runtime/python/okstra_ctl/write_policy.py +45 -6
  88. package/runtime/python/okstra_project/state.py +2 -2
  89. package/runtime/python/okstra_token_usage/__init__.py +1 -1
  90. package/runtime/python/okstra_token_usage/cli.py +3 -3
  91. package/runtime/python/okstra_token_usage/report.py +7 -24
  92. package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
  93. package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
  94. package/runtime/schemas/final-report-v2.0.schema.json +11 -1
  95. package/runtime/skills/okstra-inspect/facets/history.md +3 -3
  96. package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
  97. package/runtime/skills/okstra-inspect/facets/report.md +5 -5
  98. package/runtime/skills/okstra-inspect/facets/status.md +2 -2
  99. package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
  100. package/runtime/skills/okstra-run/SKILL.md +1 -1
  101. package/runtime/skills/okstra-schedule-gen/SKILL.md +1 -1
  102. package/runtime/skills/okstra-user-response/SKILL.md +3 -3
  103. package/runtime/templates/project-docs/task-index.template.md +1 -1
  104. package/runtime/templates/report-writer-prompt-preamble.md +1 -1
  105. package/runtime/validators/forbidden_actions.py +76 -5
  106. package/runtime/validators/lib/fixtures.sh +14 -10
  107. package/runtime/validators/lib/runners.sh +1 -1
  108. package/runtime/validators/validate-implementation-plan-stages.py +3 -0
  109. package/runtime/validators/validate-report-views.py +1 -1
  110. package/runtime/validators/validate-run.py +95 -37
@@ -98,6 +98,14 @@ _MATERIALIZABLE_AUDIENCES = frozenset({
98
98
  "report-writer",
99
99
  })
100
100
  _CLARIFICATION_INPUT_PREFIX = "- Clarification response:"
101
+ # Both sides of a stage judge against the same answers. The verifier used to
102
+ # get only the path under `## Inputs`, so whether it read the override was left
103
+ # to the worker: one provider read it and judged on the replacement command,
104
+ # another did not and reported the superseded plan's command as a stage defect.
105
+ _CLARIFICATION_AUTHORITY_AUDIENCES = frozenset({
106
+ "implementation-executor",
107
+ "implementation-verifier",
108
+ })
101
109
  _CLARIFICATION_AUTHORITY_HEADING = (
102
110
  "# Clarification answers carried in (authoritative)"
103
111
  )
@@ -578,8 +586,8 @@ def _resource_lines(
578
586
  ]
579
587
  bodies = [resource.text.rstrip() for resource in item.resources]
580
588
  clarification = (
581
- _eager_executor_clarification(item.clarification_input)
582
- if item.plan.audience == "implementation-executor"
589
+ _eager_clarification_authority(item.clarification_input)
590
+ if item.plan.audience in _CLARIFICATION_AUTHORITY_AUDIENCES
583
591
  else ""
584
592
  )
585
593
  if clarification:
@@ -630,7 +638,7 @@ def _resolve_clarification_input(
630
638
  return relative, path
631
639
 
632
640
 
633
- def _eager_executor_clarification(
641
+ def _eager_clarification_authority(
634
642
  clarification_input: tuple[str, Path] | None,
635
643
  ) -> str:
636
644
  if clarification_input is None:
@@ -924,7 +932,7 @@ def _validate_existing_clarification_metadata(
924
932
  item,
925
933
  "Clarification response",
926
934
  )
927
- _validate_eager_executor_clarification_authority(
935
+ _validate_eager_clarification_authority(
928
936
  context,
929
937
  item,
930
938
  text,
@@ -932,14 +940,14 @@ def _validate_existing_clarification_metadata(
932
940
  )
933
941
 
934
942
 
935
- def _validate_eager_executor_clarification_authority(
943
+ def _validate_eager_clarification_authority(
936
944
  context: _MaterializationContext,
937
945
  item: _PromptItem,
938
946
  text: str,
939
947
  expected: str | None,
940
948
  ) -> None:
941
949
  authority_expected = (
942
- item.plan.audience == "implementation-executor"
950
+ item.plan.audience in _CLARIFICATION_AUTHORITY_AUDIENCES
943
951
  and context.request.delivery_mode is PromptDeliveryMode.EAGER_INCLUDE
944
952
  and bool(_clarification_body(item.clarification_input))
945
953
  )
@@ -951,7 +959,7 @@ def _validate_eager_executor_clarification_authority(
951
959
  ]
952
960
  if not authority_expected and not heading_indexes:
953
961
  return
954
- if expected is None or len(heading_indexes) != 1:
962
+ if not authority_expected or expected is None:
955
963
  _raise_existing_clarification_invalid(item, "clarification authority")
956
964
  expected_lines = [
957
965
  _CLARIFICATION_AUTHORITY_HEADING,
@@ -961,11 +969,17 @@ def _validate_eager_executor_clarification_authority(
961
969
  f"{_CLARIFICATION_SOURCE_PREFIX} `{expected}`",
962
970
  "",
963
971
  ]
964
- heading_index = heading_indexes[0]
965
- authority_lines = lines[
966
- heading_index:heading_index + len(expected_lines)
972
+ # The block okstra injects is what this check owns; the carry-in body that
973
+ # follows it is the user's own document and may legitimately open with the
974
+ # same heading. Requiring the phrase to be unique made such a document
975
+ # undispatchable — the run failed `existing_prompt_invalid` with nothing in
976
+ # any contract telling the author to avoid that title.
977
+ matches = [
978
+ index
979
+ for index in heading_indexes
980
+ if lines[index:index + len(expected_lines)] == expected_lines
967
981
  ]
968
- if not authority_expected or authority_lines != expected_lines:
982
+ if not matches or any(index < matches[0] for index in heading_indexes):
969
983
  _raise_existing_clarification_invalid(item, "clarification authority")
970
984
 
971
985
 
@@ -1167,7 +1181,7 @@ def _result_paths(
1167
1181
  worker_result = _required_string(state, "resultPath")
1168
1182
  if worker_id != REPORT_WRITER_WORKER_ID:
1169
1183
  return worker_result, None
1170
- expected_report = _required_string(context.manifest, "expectedReportPath")
1184
+ expected_report = _required_string(context.manifest, "expectedReportRecordPath")
1171
1185
  return str(final_report_data_path(Path(expected_report))), worker_result
1172
1186
 
1173
1187
 
@@ -169,11 +169,13 @@ def format_show(row: dict) -> str:
169
169
  f"leadModel : {row.get('leadModel')}\n"
170
170
  f"validation : {row.get('validation')}\n"
171
171
  f"runDirRel : {row.get('runDirRel')}\n"
172
- f"finalReportRel : {row.get('finalReportRel')}\n"
172
+ f"finalReportRecordRel : {row.get('finalReportRecordRel') or row.get('finalReportRel')}\n"
173
173
  f"invocationFile : {inv}\n"
174
174
  )
175
175
 
176
176
 
177
177
  def absolute_final_report_path(row: dict) -> Optional[Path]:
178
- target = resolve_under_root(row.get("projectRoot", ""), row.get("finalReportRel", ""))
178
+ from .final_report_paths import index_report_record_rel
179
+
180
+ target = resolve_under_root(row.get("projectRoot", ""), index_report_record_rel(row))
179
181
  return target.resolve() if target is not None else None
@@ -132,7 +132,7 @@ def _render_context(
132
132
  )
133
133
  for row in siblings:
134
134
  lines.append(
135
- f"- {row.get('taskKey')}: {row.get('latestRunStatus') or 'unknown'} {row.get('latestReportPath') or ''}"
135
+ f"- {row.get('taskKey')}: {row.get('latestRunStatus') or 'unknown'} {row.get('latestReportRecordPath') or ''}"
136
136
  )
137
137
  if not siblings:
138
138
  lines.append("- none")
@@ -89,7 +89,7 @@ def _snapshot_child(project_root: Path, child: dict) -> dict:
89
89
  workflow.get("nextRecommendedPhase")
90
90
  ),
91
91
  "latestRunStatus": str(manifest.get("latestRunStatus") or manifest.get("currentStatus") or ""),
92
- "latestReportPath": str(manifest.get("latestReportPath") or ""),
92
+ "latestReportRecordPath": str(manifest.get("latestReportRecordPath") or ""),
93
93
  "finalVerdict": str(outcome.get("finalVerdict") or ""),
94
94
  "crossProjectDependencies": _list_field(outcome, "crossProjectDependencies"),
95
95
  "recommendedNextSteps": _list_field(outcome, "recommendedNextSteps"),
@@ -200,7 +200,7 @@ def _hydrate_active_run(
200
200
  "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
201
201
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
202
202
  "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
203
- "finalReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
203
+ "finalReportRecordPath": ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
204
204
  "convergenceStatePath": ctx.get("CONVERGENCE_STATE_RELATIVE_PATH", ""),
205
205
  "finalStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
206
206
  "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
@@ -651,7 +651,7 @@ def _relative_file_fields(project_root: Path, paths: Mapping[str, Any]) -> dict[
651
651
  "CODEX_WORKER_PROMPT_RELATIVE_PATH": _rel(project_root, paths["codex_worker_prompt"]),
652
652
  "ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH": _rel(project_root, paths["antigravity_worker_prompt"]),
653
653
  "REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH": _rel(project_root, paths["report_writer_worker_prompt"]),
654
- "FINAL_REPORT_RELATIVE_PATH": _rel(project_root, paths["final_report"]),
654
+ "FINAL_REPORT_RECORD_RELATIVE_PATH": _rel(project_root, paths["final_report"]),
655
655
  "CONVERGENCE_STATE_RELATIVE_PATH": _rel(
656
656
  project_root, paths["convergence_state"]
657
657
  ),
@@ -83,11 +83,12 @@ def find_asset_root(
83
83
 
84
84
 
85
85
  _STAGED_TASK_TYPES = ("implementation", "final-verification")
86
- # 발견용: bash `find -name 'final-report-*.md'` 와 같은 범위 — seq 이전 세대의
87
- # 타임스탬프 파일명도 잡아야 옛 번들에서 조용히 실패하지 않는다.
88
- _REPORT_NAME_RE = re.compile(r"^final-report-.+\.md$")
89
- # seq 추출용: 정본 이름 `final-report-<task-type>-<NNN>.md` 일 때만.
90
- _REPORT_SEQ_RE = re.compile(r"^final-report-.+-(?P<seq>\d{3,})\.md$")
86
+ # 발견용: 정본 `final-report-*.data.json` 과, 옛 원장 행이 가리키는 열람본.
87
+ _REPORT_NAME_RE = re.compile(r"^final-report-.+\.(?:md|data\.json)$")
88
+ # seq 추출용: `final-report-<task-type>-<NNN>.data.json` (옛 열람본 이름도).
89
+ _REPORT_SEQ_RE = re.compile(
90
+ r"^final-report-.+-(?P<seq>\d{3,})\.(?:md|data\.json)$"
91
+ )
91
92
  _TASKS_SEGMENTS = Path(TASKS_RELATIVE).parts
92
93
 
93
94
 
@@ -95,10 +96,12 @@ def _newest_report(reports_dir: Path) -> Optional[Path]:
95
96
  """reports 디렉터리에서 mtime 최신 final-report. 동률이면 basename 큰 쪽."""
96
97
  if not reports_dir.is_dir():
97
98
  return None
98
- found = [
99
+ files = [
99
100
  entry for entry in reports_dir.iterdir()
100
101
  if entry.is_file() and _REPORT_NAME_RE.match(entry.name)
101
102
  ]
103
+ records = [entry for entry in files if entry.name.endswith(".data.json")]
104
+ found = records or files
102
105
  if not found:
103
106
  return None
104
107
  return max(found, key=lambda p: (p.stat().st_mtime, p.name))
@@ -182,7 +185,7 @@ class RunRef:
182
185
  def report(self) -> Path:
183
186
  if self.report_override is not None:
184
187
  return self.report_override
185
- return self.reports_dir / f"final-report{self._suffix}.md"
188
+ return self.reports_dir / f"final-report{self._suffix}.data.json"
186
189
 
187
190
  @property
188
191
  def manifest(self) -> Path:
@@ -404,7 +407,7 @@ def _rel(project_root: Path, target: Path) -> str:
404
407
  def resolve_under_root(
405
408
  project_root: str | Path | None, rel: str | None
406
409
  ) -> Path | None:
407
- """글로벌 인덱스의 신뢰 불가 상대경로 필드(runDirRel/finalReportRel/
410
+ """글로벌 인덱스의 신뢰 불가 상대경로 필드(runDirRel/finalReportRecordRel/
408
411
  teamStatePath 등)를 project_root 기준 절대경로로 푼다(_rel 의 역).
409
412
 
410
413
  이 필드들은 신뢰할 수 없어 절대경로나 '..' 가 들어오면 결합 결과가
@@ -538,7 +541,7 @@ def compute_run_paths(
538
541
  codex_worker_prompt = run_prompts / f"codex-worker-prompt{suffixes['prompts']}.md"
539
542
  antigravity_worker_prompt = run_prompts / f"antigravity-worker-prompt{suffixes['prompts']}.md"
540
543
  report_writer_worker_prompt = run_prompts / f"report-writer-worker-prompt{suffixes['prompts']}.md"
541
- final_report = run_reports / f"final-report{suffixes['reports']}.md"
544
+ final_report = run_reports / f"final-report{suffixes['reports']}.data.json"
542
545
  final_status = run_status / f"final{suffixes['status']}.status"
543
546
  team_state = run_state / f"team-state{suffixes['state']}.json"
544
547
  active_run_context = run_state / f"active-run-context{suffixes['state']}.json"
@@ -712,7 +715,7 @@ def compute_run_paths(
712
715
  ("CODEX_WORKER_PROMPT_RELATIVE_PATH", codex_worker_prompt),
713
716
  ("ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH", antigravity_worker_prompt),
714
717
  ("REPORT_WRITER_WORKER_PROMPT_RELATIVE_PATH", report_writer_worker_prompt),
715
- ("FINAL_REPORT_RELATIVE_PATH", final_report),
718
+ ("FINAL_REPORT_RECORD_RELATIVE_PATH", final_report),
716
719
  ("FINAL_STATUS_RELATIVE_PATH", final_status),
717
720
  ("TEAM_STATE_RELATIVE_PATH", team_state),
718
721
  ("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", active_run_context),
@@ -4,8 +4,8 @@
4
4
  원장) 과 approved-plan(final-report) 이 모두 이 루트 기준으로 놓인다.
5
5
 
6
6
  두 진입점이 같은 layout 규칙을 공유한다:
7
- - final-verification 은 사용자가 넘긴 approved-plan 파일 경로에서 역산한다
8
- (``<plan_run_root>/reports/final-report-...md`` → ``parents[1]``).
7
+ - final-verification 은 사용자가 넘긴 approved-plan 정본 경로에서 역산한다
8
+ (``<plan_run_root>/reports/final-report-....data.json`` → ``parents[1]``).
9
9
  - container 는 approved-plan 입력이 없으므로 task-key 로 planning run 디렉터리를
10
10
  스캔해 latest done run 을 고른다.
11
11
  """
@@ -17,7 +17,9 @@ from typing import NamedTuple
17
17
 
18
18
  from .stage_targets import PrepareError
19
19
 
20
- _FINAL_REPORT_RE = re.compile(r"final-report-implementation-planning-(\d+)\.md$")
20
+ _FINAL_REPORT_RE = re.compile(
21
+ r"final-report-implementation-planning-(\d+)\.data\.json$"
22
+ )
21
23
 
22
24
 
23
25
  class PlanRun(NamedTuple):
@@ -29,7 +31,9 @@ class PlanRun(NamedTuple):
29
31
  def list_implementation_planning_reports(reports_dir: Path) -> list[Path]:
30
32
  """Return numbered implementation-planning reports in latest-first order."""
31
33
  numbered: list[tuple[int, Path]] = []
32
- for report in reports_dir.glob("final-report-implementation-planning-*.md"):
34
+ for report in reports_dir.glob(
35
+ "final-report-implementation-planning-*.data.json"
36
+ ):
33
37
  match = _FINAL_REPORT_RE.fullmatch(report.name)
34
38
  if match:
35
39
  numbered.append((int(match.group(1)), report))
@@ -40,7 +44,7 @@ def list_implementation_planning_reports(reports_dir: Path) -> list[Path]:
40
44
  def plan_run_root_from_approved_plan(approved_plan_path: str | Path) -> Path:
41
45
  """approved-plan(final-report) 경로에서 plan_run_root 를 역산한다.
42
46
 
43
- final-report 는 ``<plan_run_root>/reports/final-report-...md`` 에 놓이므로
47
+ 정본은 ``<plan_run_root>/reports/final-report-....data.json`` 에 놓이므로
44
48
  ``parents[1]`` 이 run-root 다. run.py(final-verification) 와 container 가
45
49
  이 한 규칙을 공유한다."""
46
50
  return Path(approved_plan_path).resolve().parents[1]
@@ -18,6 +18,7 @@ from okstra_ctl.incremental_scope import preview_link_availability_for_report
18
18
  from okstra_ctl.paths import task_timeline_file
19
19
  from okstra_project import read_task_key
20
20
  from okstra_ctl.run_context import dir_flock
21
+ from okstra_ctl.final_report_paths import timeline_report_record_rel
21
22
  from okstra_ctl.task_target import resolve_task_root, project_rel
22
23
 
23
24
  NOTE_KINDS = ("verification-evidence", "decision-draft", "analysis-note")
@@ -51,7 +52,7 @@ def _latest_planning_report(runs: list[dict], project_root: Path) -> Path | None
51
52
  continue
52
53
  if run.get("taskType") != "implementation-planning":
53
54
  continue
54
- relative = str(run.get("reportPath") or "")
55
+ relative = timeline_report_record_rel(run)
55
56
  if not relative:
56
57
  continue
57
58
  report = project_root / relative
@@ -112,7 +113,7 @@ def assemble_recap(task_root: Path, project_root: Path) -> dict:
112
113
  "nextRecommendedPhase": next_phase.promote(
113
114
  snap.get("nextRecommendedPhase")
114
115
  ),
115
- "reportPath": run.get("reportPath", ""),
116
+ "reportRecordPath": timeline_report_record_rel(run),
116
117
  })
117
118
  prev_phase = cur
118
119
  # latestPhaseStates 는 항상 가장 최근 run 의 상태를 반영해야 한다.
@@ -135,7 +135,9 @@ def _reconcile_active_locked(home: Path, *,
135
135
  survivors.append(row)
136
136
  continue
137
137
  project_root = Path(row.get("projectRoot", ""))
138
- final_rel = row.get("finalReportRel", "")
138
+ from .final_report_paths import index_report_record_rel
139
+
140
+ final_rel = index_report_record_rel(row)
139
141
  final_abs = project_root / final_rel if final_rel else None
140
142
  if final_abs and final_abs.is_file():
141
143
  validation_status = _read_run_manifest_validation(
@@ -591,7 +591,7 @@ def _active_run(ctx: dict) -> dict:
591
591
  "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
592
592
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
593
593
  "promptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
594
- "finalReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
594
+ "finalReportRecordPath": ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
595
595
  "convergenceStatePath": ctx.get("CONVERGENCE_STATE_RELATIVE_PATH", ""),
596
596
  "finalStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
597
597
  "validatorScriptPath": ctx.get("RUN_VALIDATOR_RELATIVE_PATH", ""),
@@ -795,7 +795,7 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
795
795
  "artifacts": {
796
796
  "leadPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
797
797
  "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
798
- "finalReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
798
+ "finalReportRecordPath": ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
799
799
  "finalStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
800
800
  "workerResultsDirectoryPath": ctx.get("WORKER_RESULTS_RELATIVE_PATH", ""),
801
801
  },
@@ -952,7 +952,7 @@ def render_task_catalog_discovery(output_path: str, ctx: dict) -> None:
952
952
  "latestTeamStatePath": s(latest_run, "teamStatePath"),
953
953
  "latestRunStatus": s(manifest, "latestRunStatus")
954
954
  or s(latest_run, "status"),
955
- "latestReportPath": s(manifest, "latestReportPath")
955
+ "latestReportRecordPath": s(manifest, "latestReportRecordPath")
956
956
  or s(latest_run, "reportPath"),
957
957
  "latestResumeCommandPath": s(manifest, "latestResumeCommandPath")
958
958
  or s(latest_run, "resumeCommandPath"),
@@ -1017,8 +1017,8 @@ def render_latest_task_discovery(output_path: str, ctx: dict) -> None:
1017
1017
  "latestRunPromptsPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1018
1018
  "latestPromptSnapshotPath": ctx.get("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", ""),
1019
1019
  "latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1020
- "latestReportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH", ""),
1021
- "expectedReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
1020
+ "latestReportRecordPath": ctx.get("LATEST_REPORT_RECORD_RELATIVE_PATH", ""),
1021
+ "expectedReportRecordPath": ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
1022
1022
  "expectedStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
1023
1023
  "latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1024
1024
  "historyTimelinePath": ctx.get("TIMELINE_RELATIVE_PATH", ""),
@@ -1297,8 +1297,8 @@ def _derive_latest_pointers(existing: dict, ctx: dict, current_report_relative:
1297
1297
  "latestTeamStatePath": existing_checkpoint.get(
1298
1298
  "latestTeamStatePath", existing.get("teamStatePath", "")
1299
1299
  ),
1300
- "latestReportPath": existing_checkpoint.get(
1301
- "latestReportPath", existing.get("latestReportPath", "")
1300
+ "latestReportRecordPath": existing_checkpoint.get(
1301
+ "latestReportRecordPath", existing.get("latestReportRecordPath", "")
1302
1302
  ),
1303
1303
  "latestResumeCommandPath": existing_checkpoint.get(
1304
1304
  "latestResumeCommandPath", existing.get("latestResumeCommandPath", "")
@@ -1310,7 +1310,7 @@ def _derive_latest_pointers(existing: dict, ctx: dict, current_report_relative:
1310
1310
  or ctx.get("CURRENT_RUN_STATUS", ""),
1311
1311
  "latestRunPromptsPath": existing.get("latestRunPromptsPath", "")
1312
1312
  or ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1313
- "latestReportPath": existing.get("latestReportPath", "")
1313
+ "latestReportRecordPath": existing.get("latestReportRecordPath", "")
1314
1314
  or current_report_relative,
1315
1315
  "latestTeamStatePath": existing.get("teamStatePath", "")
1316
1316
  or ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
@@ -1324,14 +1324,14 @@ def _derive_latest_pointers(existing: dict, ctx: dict, current_report_relative:
1324
1324
  "latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
1325
1325
  "latestRunManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
1326
1326
  "latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1327
- "latestReportPath": current_report_relative,
1327
+ "latestReportRecordPath": current_report_relative,
1328
1328
  "latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
1329
1329
  },
1330
1330
  "latestRunPath": ctx.get("LATEST_RUN_RELATIVE_PATH", ""),
1331
1331
  "latestRunStatus": ctx.get("CURRENT_RUN_STATUS", ""),
1332
1332
  "latestRunPromptsPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1333
- "latestReportPath": current_report_relative
1334
- or existing.get("latestReportPath", ""),
1333
+ "latestReportRecordPath": current_report_relative
1334
+ or existing.get("latestReportRecordPath", ""),
1335
1335
  "latestTeamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1336
1336
  "latestResumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", "")
1337
1337
  or existing.get("latestResumeCommandPath", ""),
@@ -1357,8 +1357,8 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1357
1357
  catalog[item]["role"] for item in reviewers
1358
1358
  ]
1359
1359
  related_tasks = json.loads(ctx.get("RELATED_TASKS_JSON", "[]"))
1360
- current_report_relative = ctx.get("LATEST_REPORT_RELATIVE_PATH") or ctx.get(
1361
- "FINAL_REPORT_RELATIVE_PATH", ""
1360
+ current_report_relative = ctx.get("LATEST_REPORT_RECORD_RELATIVE_PATH") or ctx.get(
1361
+ "FINAL_REPORT_RECORD_RELATIVE_PATH", ""
1362
1362
  )
1363
1363
  existing_workflow = (
1364
1364
  existing.get("workflow", {})
@@ -1385,7 +1385,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1385
1385
  latest_run_relative = pointers["latestRunPath"]
1386
1386
  latest_run_status = pointers["latestRunStatus"]
1387
1387
  latest_run_prompts_relative = pointers["latestRunPromptsPath"]
1388
- latest_report_relative = pointers["latestReportPath"]
1388
+ latest_report_relative = pointers["latestReportRecordPath"]
1389
1389
  latest_team_state_relative = pointers["latestTeamStatePath"]
1390
1390
  latest_resume_command_relative = pointers["latestResumeCommandPath"]
1391
1391
  convergence_block = _build_convergence_block(ctx)
@@ -1434,7 +1434,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
1434
1434
  "latestRunPath": latest_run_relative,
1435
1435
  "latestRunStatus": latest_run_status,
1436
1436
  "latestRunPromptsPath": latest_run_prompts_relative,
1437
- "latestReportPath": latest_report_relative,
1437
+ "latestReportRecordPath": latest_report_relative,
1438
1438
  "latestResumeCommandPath": latest_resume_command_relative,
1439
1439
  "teamStatePath": latest_team_state_relative,
1440
1440
  "fixCycles": fix_cycles.summarize(
@@ -1762,7 +1762,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
1762
1762
  ),
1763
1763
  "workerPromptsDirectoryPath": ctx.get("RUN_PROMPTS_RELATIVE_PATH", ""),
1764
1764
  "workerPromptPathByWorkerId": worker_prompt_paths,
1765
- "expectedReportPath": ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
1765
+ "expectedReportRecordPath": ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
1766
1766
  "expectedStatusPath": ctx.get("FINAL_STATUS_RELATIVE_PATH", ""),
1767
1767
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
1768
1768
  "activeRunContextPath": ctx.get("ACTIVE_RUN_CONTEXT_RELATIVE_PATH", ""),
@@ -1903,7 +1903,7 @@ _IMMUTABLE_INVOCATION_KEYS = (
1903
1903
  "workerPromptsDirectoryPath",
1904
1904
  "workerPromptPathByWorkerId",
1905
1905
  "workerResultsDirectoryPath",
1906
- "expectedReportPath",
1906
+ "expectedReportRecordPath",
1907
1907
  "reportTemplatePath",
1908
1908
  )
1909
1909
 
@@ -2091,8 +2091,8 @@ def render_timeline(timeline_path: str, ctx: dict) -> None:
2091
2091
  "workerPromptPathByWorkerId": {
2092
2092
  item: catalog[item]["promptPath"] for item in reviewers
2093
2093
  },
2094
- "reportPath": ctx.get("LATEST_REPORT_RELATIVE_PATH")
2095
- or ctx.get("FINAL_REPORT_RELATIVE_PATH", ""),
2094
+ "reportRecordPath": ctx.get("LATEST_REPORT_RECORD_RELATIVE_PATH")
2095
+ or ctx.get("FINAL_REPORT_RECORD_RELATIVE_PATH", ""),
2096
2096
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
2097
2097
  "resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
2098
2098
  "relatedTasks": json.loads(ctx.get("RELATED_TASKS_JSON", "[]")),
@@ -2172,7 +2172,7 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
2172
2172
  f"- Label: `{checkpoint.get('label', 'unknown')}`",
2173
2173
  f"- Run manifest: `{checkpoint.get('latestRunManifestPath', ctx.get('RUN_MANIFEST_RELATIVE_PATH', ''))}`",
2174
2174
  f"- Team state: `{checkpoint.get('latestTeamStatePath', ctx.get('TEAM_STATE_RELATIVE_PATH', ''))}`",
2175
- f"- Report: `{checkpoint.get('latestReportPath', task_manifest.get('latestReportPath', '--')) or '--'}`",
2175
+ f"- Report: `{checkpoint.get('latestReportRecordPath', task_manifest.get('latestReportRecordPath', '--')) or '--'}`",
2176
2176
  f"- Resume command: `{checkpoint.get('latestResumeCommandPath', task_manifest.get('latestResumeCommandPath', '--')) or '--'}`",
2177
2177
  ]
2178
2178
  rc = (
@@ -2236,8 +2236,8 @@ def render_task_index(template_path: str, output_path: str, ctx: dict) -> None:
2236
2236
  "{{LATEST_RUN_RELATIVE_PATH}}": task_manifest.get(
2237
2237
  "latestRunPath", ctx.get("LATEST_RUN_RELATIVE_PATH", "")
2238
2238
  ),
2239
- "{{LATEST_REPORT_RELATIVE_PATH}}": task_manifest.get(
2240
- "latestReportPath", ctx.get("LATEST_REPORT_RELATIVE_PATH", "")
2239
+ "{{LATEST_REPORT_RECORD_RELATIVE_PATH}}": task_manifest.get(
2240
+ "latestReportRecordPath", ctx.get("LATEST_REPORT_RECORD_RELATIVE_PATH", "")
2241
2241
  ),
2242
2242
  "{{TEAM_STATE_RELATIVE_PATH}}": task_manifest.get(
2243
2243
  "teamStatePath", ctx.get("TEAM_STATE_RELATIVE_PATH", "")
@@ -338,7 +338,7 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
338
338
  ctx.task_key,
339
339
  ],
340
340
  ),
341
- (STEP_VALIDATE_RUN, _validate_run_command(ctx, markdown_path)),
341
+ (STEP_VALIDATE_RUN, _validate_run_command(ctx, ctx.data_path)),
342
342
  (
343
343
  STEP_TEARDOWN_STAGES,
344
344
  ["<in-process>", "teardown-stages", str(ctx.data_path)],
@@ -382,14 +382,14 @@ def _teardown_stage_worktrees(
382
382
  return _done(result)
383
383
 
384
384
 
385
- def _validate_run_command(ctx: FinalizeContext, markdown_path: Path) -> list[str]:
385
+ def _validate_run_command(ctx: FinalizeContext, report_record_path: Path) -> list[str]:
386
386
  command = [
387
387
  sys.executable,
388
388
  str(resolve_workspace_validator(ctx.workspace_root, "validate-run.py")),
389
389
  "--team-state",
390
390
  str(ctx.team_state_path),
391
391
  "--report",
392
- str(markdown_path),
392
+ str(report_record_path),
393
393
  "--run-manifest",
394
394
  str(ctx.manifest_path),
395
395
  "--task-manifest",
@@ -611,7 +611,7 @@ def _parser() -> argparse.ArgumentParser:
611
611
  parser.add_argument(
612
612
  "--report",
613
613
  required=True,
614
- help="final-report markdown path (its .data.json sibling is derived)",
614
+ help="final-report record (.data.json); a leftover markdown path still locates the sibling record",
615
615
  )
616
616
  parser.add_argument("--workspace-root", required=True)
617
617
  parser.add_argument(
@@ -86,7 +86,7 @@ def _collect_task(entry: dict, project_root: Path) -> dict:
86
86
  ),
87
87
  "latestRunStatus": entry.get("latestRunStatus", ""),
88
88
  "updatedAt": entry.get("updatedAt", ""),
89
- "reportPath": entry.get("latestReportPath", ""),
89
+ "reportPath": entry.get("latestReportRecordPath", ""),
90
90
  "runCount": len(runs),
91
91
  "cpuSumMs": time_agg["grandTotal"]["cpuSumMs"],
92
92
  "wallClockMs": sum(r["wallClockMs"] for r in time_agg["perRunWallClock"]),