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.
- package/README.md +2 -2
- package/dist/commands/execute/plan-verify.mjs +1 -1
- package/dist/commands/execute/worktree-status.mjs +8 -2
- package/dist/commands/execute/worktree-status.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +1 -1
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/report/render-final-report.mjs +3 -3
- package/docs/architecture/storage-model.md +3 -3
- package/docs/architecture.md +10 -9
- package/docs/cli.md +11 -13
- package/docs/for-ai/skills/okstra-inspect.md +3 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +10 -11
- package/docs/task-process/implementation-planning.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +11 -12
- package/runtime/bin/lib/okstra/globals.sh +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +1 -1
- package/runtime/bin/lib/okstra/usage.sh +11 -9
- package/runtime/bin/lib/okstra-ctl/cmd-rerun.sh +1 -1
- package/runtime/bin/okstra-central.sh +2 -2
- package/runtime/bin/okstra-render-final-report.py +1 -1
- package/runtime/bin/okstra-token-usage.py +1 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/adapters/cmux.md +6 -1
- package/runtime/prompts/lead/context-loader.md +3 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
- package/runtime/prompts/lead/plan-body-verification.md +3 -3
- package/runtime/prompts/lead/report-writer.md +21 -20
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +5 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +2 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +9 -5
- package/runtime/prompts/profiles/implementation.md +4 -4
- package/runtime/prompts/profiles/improvement-discovery.md +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +5 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -4
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +71 -9
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +5 -4
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +55 -3
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -3
- package/runtime/python/okstra_ctl/analysis_packet.py +21 -0
- package/runtime/python/okstra_ctl/backfill.py +12 -5
- package/runtime/python/okstra_ctl/consumers.py +70 -3
- package/runtime/python/okstra_ctl/convergence_engine.py +43 -17
- package/runtime/python/okstra_ctl/dispatch_core.py +47 -11
- package/runtime/python/okstra_ctl/dispatch_state.py +20 -19
- package/runtime/python/okstra_ctl/domain/worker_exec.py +13 -34
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +128 -0
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +5 -0
- package/runtime/python/okstra_ctl/final_report_paths.py +77 -1
- package/runtime/python/okstra_ctl/handoff.py +1 -2
- package/runtime/python/okstra_ctl/implementation_outcome.py +1 -1
- package/runtime/python/okstra_ctl/index.py +4 -4
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +26 -12
- package/runtime/python/okstra_ctl/listing.py +4 -2
- package/runtime/python/okstra_ctl/manager_launch.py +1 -1
- package/runtime/python/okstra_ctl/manager_sync.py +1 -1
- package/runtime/python/okstra_ctl/path_hints.py +2 -2
- package/runtime/python/okstra_ctl/paths.py +13 -10
- package/runtime/python/okstra_ctl/plan_run_root.py +9 -5
- package/runtime/python/okstra_ctl/recap.py +3 -2
- package/runtime/python/okstra_ctl/reconcile.py +3 -1
- package/runtime/python/okstra_ctl/render.py +22 -22
- package/runtime/python/okstra_ctl/report_finalize.py +4 -4
- package/runtime/python/okstra_ctl/rollup.py +1 -1
- package/runtime/python/okstra_ctl/run.py +139 -284
- package/runtime/python/okstra_ctl/run_audit.py +5 -5
- package/runtime/python/okstra_ctl/run_index_row.py +2 -2
- package/runtime/python/okstra_ctl/session_transcript.py +89 -0
- package/runtime/python/okstra_ctl/stage_ledger.py +72 -0
- package/runtime/python/okstra_ctl/stage_map.py +28 -29
- package/runtime/python/okstra_ctl/stage_targets.py +61 -0
- package/runtime/python/okstra_ctl/user_response.py +97 -12
- package/runtime/python/okstra_ctl/wizard.py +43 -65
- package/runtime/python/okstra_ctl/worker_prompt_body.py +6 -7
- package/runtime/python/okstra_ctl/worker_runner.py +76 -213
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/wrapper_status.py +23 -0
- package/runtime/python/okstra_ctl/write_policy.py +45 -6
- package/runtime/python/okstra_project/state.py +2 -2
- package/runtime/python/okstra_token_usage/__init__.py +1 -1
- package/runtime/python/okstra_token_usage/cli.py +3 -3
- package/runtime/python/okstra_token_usage/report.py +7 -24
- package/runtime/schemas/convergence-groups-v1.0.schema.json +1 -1
- package/runtime/schemas/convergence-groups-v2.0.schema.json +1 -1
- package/runtime/schemas/final-report-v2.0.schema.json +11 -1
- package/runtime/skills/okstra-inspect/facets/history.md +3 -3
- package/runtime/skills/okstra-inspect/facets/recap.md +1 -1
- package/runtime/skills/okstra-inspect/facets/report.md +5 -5
- package/runtime/skills/okstra-inspect/facets/status.md +2 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +3 -3
- package/runtime/templates/project-docs/task-index.template.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -1
- package/runtime/validators/forbidden_actions.py +76 -5
- package/runtime/validators/lib/fixtures.sh +14 -10
- package/runtime/validators/lib/runners.sh +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +3 -0
- package/runtime/validators/validate-report-views.py +1 -1
- 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
|
-
|
|
582
|
-
if item.plan.audience
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
|
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, "
|
|
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"
|
|
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
|
-
|
|
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('
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
# 발견용:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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}.
|
|
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/
|
|
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']}.
|
|
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
|
-
("
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1021
|
-
"
|
|
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
|
-
"
|
|
1301
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1334
|
-
or existing.get("
|
|
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("
|
|
1361
|
-
"
|
|
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["
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
2095
|
-
or ctx.get("
|
|
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('
|
|
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
|
-
"{{
|
|
2240
|
-
"
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
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("
|
|
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"]),
|