okstra 0.179.1 → 0.180.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 +1 -1
- package/dist/cli-registry.mjs +14 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +10 -10
- package/docs/cli.md +11 -8
- package/docs/project-structure-overview.md +15 -6
- package/docs/task-process/implementation-planning.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/prompts/launch.template.md +6 -5
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +19 -18
- package/runtime/prompts/lead/plan-body-verification.md +43 -19
- package/runtime/prompts/lead/report-writer.md +64 -423
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +13 -11
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/agent_activity.py +23 -3
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +6 -6
- package/runtime/python/okstra_ctl/analysis_packet.py +43 -2
- package/runtime/python/okstra_ctl/approval_decisions.py +327 -0
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +84 -13
- package/runtime/python/okstra_ctl/dispatch_state.py +29 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +24 -15
- package/runtime/python/okstra_ctl/incremental_carry.py +128 -16
- package/runtime/python/okstra_ctl/incremental_scope.py +4 -1
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +12 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +113 -16
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/render.py +48 -1
- package/runtime/python/okstra_ctl/render_final_report.py +26 -20
- package/runtime/python/okstra_ctl/report_assembly.py +354 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +60 -22
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_markdown.py +69 -8
- package/runtime/python/okstra_ctl/report_narrative.py +319 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/run.py +25 -9
- package/runtime/python/okstra_ctl/schema_excerpt.py +94 -14
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -4
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +70 -22
- package/runtime/python/okstra_ctl/team.py +1 -1
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +35 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/schemas/final-report-v3.0.schema.json +10210 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/templates/report-writer-prompt-preamble.md +15 -21
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/validators/validate-run.py +325 -21
|
@@ -47,6 +47,7 @@ from .execution_manifest import (
|
|
|
47
47
|
)
|
|
48
48
|
from .execution_mutation_audit import ExecutionMutationAudit, MutationSnapshot
|
|
49
49
|
from .final_report_paths import final_report_data_path
|
|
50
|
+
from .report_inputs import report_narrative_path, uses_report_contract_v3
|
|
50
51
|
from .worker_prompt_body import REPORT_WRITER_WORKER_ID
|
|
51
52
|
from .worker_prompt_contract import (
|
|
52
53
|
PromptRecord,
|
|
@@ -152,7 +153,11 @@ class WorkerJob:
|
|
|
152
153
|
completion_paths: tuple[Path, ...]
|
|
153
154
|
worktree_path: str
|
|
154
155
|
role: str
|
|
155
|
-
|
|
156
|
+
# None 은 "이 디스패치가 예산을 정하지 않는다" 는 뜻이고, 그때 예산은
|
|
157
|
+
# 역할이 정한다(`domain/worker_role.role_spec`). 여기에 숫자 기본값을 두면
|
|
158
|
+
# 역할별 예산이 영영 도달 불가능해진다 — 엔트리포인트는 이 자리가 비었을
|
|
159
|
+
# 때만 역할을 보기 때문이다.
|
|
160
|
+
idle_timeout_seconds: int | None
|
|
156
161
|
dispatch_kind: str
|
|
157
162
|
invocation_id: str = ""
|
|
158
163
|
audience: str = ""
|
|
@@ -188,7 +193,7 @@ class WorkerJob:
|
|
|
188
193
|
str(self.prompt_path),
|
|
189
194
|
self.worktree_path,
|
|
190
195
|
self.wrapper_role,
|
|
191
|
-
|
|
196
|
+
self._idle_timeout_argument,
|
|
192
197
|
"--presentation",
|
|
193
198
|
self._presentation(),
|
|
194
199
|
]
|
|
@@ -201,6 +206,18 @@ class WorkerJob:
|
|
|
201
206
|
argv += ["--session-id", self.session_id]
|
|
202
207
|
return argv
|
|
203
208
|
|
|
209
|
+
@property
|
|
210
|
+
def _idle_timeout_argument(self) -> str:
|
|
211
|
+
"""엔트리포인트가 읽는 유휴 예산 자리.
|
|
212
|
+
|
|
213
|
+
비워 두면 엔트리포인트가 역할의 예산을 쓴다. 이 자리를 항상 채우던
|
|
214
|
+
동안에는 `worker_request.idle_timeout` 의 역할 분기가 한 번도 실행되지
|
|
215
|
+
않았고, executor/verifier 의 1500s 도 함께 죽어 있었다.
|
|
216
|
+
"""
|
|
217
|
+
if self.idle_timeout_seconds is None:
|
|
218
|
+
return ""
|
|
219
|
+
return str(self.idle_timeout_seconds)
|
|
220
|
+
|
|
204
221
|
@property
|
|
205
222
|
def wrapper_role(self) -> str:
|
|
206
223
|
"""The canonical role the entrypoint's role positional is read as.
|
|
@@ -707,6 +724,10 @@ def record_verified_agent_dispatch(
|
|
|
707
724
|
snapshot = ExecutionMutationAudit().snapshot(
|
|
708
725
|
(write_policy,),
|
|
709
726
|
orchestrator_paths=(
|
|
727
|
+
# run 산출물 트리 전체. 같은 run 의 다른 라운드가 감사 창
|
|
728
|
+
# 안에 남기는 프롬프트·로그·사이드카를 파일 단위로 미리 셀
|
|
729
|
+
# 수 없고, 그 아래는 정의상 소스가 아니다.
|
|
730
|
+
Path(run_manifest_path).resolve().parents[1],
|
|
710
731
|
run_manifest_path,
|
|
711
732
|
team_state_path,
|
|
712
733
|
Path(f"{team_state_path}.lock"),
|
|
@@ -1468,6 +1489,8 @@ def dispatch_result_path(
|
|
|
1468
1489
|
"""
|
|
1469
1490
|
if worker_id != REPORT_WRITER_WORKER_ID:
|
|
1470
1491
|
return worker_result_path
|
|
1492
|
+
if uses_report_contract_v3(manifest):
|
|
1493
|
+
return report_narrative_path(project_root, manifest)
|
|
1471
1494
|
return final_report_data_path(
|
|
1472
1495
|
resolve_required_path(project_root, manifest, "expectedReportRecordPath")
|
|
1473
1496
|
)
|
|
@@ -1487,6 +1510,8 @@ def dispatch_completion_paths(
|
|
|
1487
1510
|
"""
|
|
1488
1511
|
if worker_id != REPORT_WRITER_WORKER_ID:
|
|
1489
1512
|
return (worker_result_path,)
|
|
1513
|
+
if uses_report_contract_v3(manifest):
|
|
1514
|
+
return (report_narrative_path(project_root, manifest), worker_result_path)
|
|
1490
1515
|
data_json = final_report_data_path(
|
|
1491
1516
|
resolve_required_path(project_root, manifest, "expectedReportRecordPath")
|
|
1492
1517
|
)
|
|
@@ -1765,7 +1790,7 @@ def worker_jobs_from_file(
|
|
|
1765
1790
|
manifest: Mapping[str, Any],
|
|
1766
1791
|
active_context: Mapping[str, Any] | None = None,
|
|
1767
1792
|
backend: str,
|
|
1768
|
-
idle_timeout_seconds: int,
|
|
1793
|
+
idle_timeout_seconds: int | None,
|
|
1769
1794
|
default_dispatch_kind: str,
|
|
1770
1795
|
resolve_wrapper: Callable[[str], Path],
|
|
1771
1796
|
default_provider: Callable[[str], str],
|
|
@@ -1804,7 +1829,7 @@ def _worker_job_from_file(
|
|
|
1804
1829
|
manifest: Mapping[str, Any],
|
|
1805
1830
|
active_context: Mapping[str, Any] | None = None,
|
|
1806
1831
|
backend: str,
|
|
1807
|
-
idle_timeout_seconds: int,
|
|
1832
|
+
idle_timeout_seconds: int | None,
|
|
1808
1833
|
dispatch_kind: str,
|
|
1809
1834
|
resolve_wrapper: Callable[[str], Path],
|
|
1810
1835
|
default_provider: Callable[[str], str],
|
|
@@ -32,6 +32,10 @@ class MutationSnapshot:
|
|
|
32
32
|
scratch_digests: Mapping[str, str]
|
|
33
33
|
git_projection: Mapping[str, Any]
|
|
34
34
|
digest: str
|
|
35
|
+
# 오케스트레이터가 쓰는 경로. 파일 하나를 싣거나 디렉터리를 실어 그 트리
|
|
36
|
+
# 전체를 덮을 수 있다 — 한 배치가 무엇을 쓸지 파일 단위로 미리 셀 수 없기
|
|
37
|
+
# 때문이다(다른 라운드의 재시도 프롬프트, 로그, 상태 사이드카가 그 창에
|
|
38
|
+
# 들어온다).
|
|
35
39
|
orchestrator_paths: tuple[str, ...] = ()
|
|
36
40
|
|
|
37
41
|
def to_payload(self) -> dict[str, Any]:
|
|
@@ -445,7 +449,7 @@ def _source_changes(
|
|
|
445
449
|
snapshot.artifact_root == snapshot.root
|
|
446
450
|
and any(_is_within(path, item) for item in artifacts)
|
|
447
451
|
)
|
|
448
|
-
and path
|
|
452
|
+
and not any(_is_within(path, item) for item in orchestrator)
|
|
449
453
|
and not _excluded(Path(path), generated)
|
|
450
454
|
}
|
|
451
455
|
|
|
@@ -464,7 +468,7 @@ def _artifact_policy_failures(
|
|
|
464
468
|
unauthorized = {
|
|
465
469
|
path for path in changed
|
|
466
470
|
if not any(_is_within(path, item) for item in allowed)
|
|
467
|
-
and path
|
|
471
|
+
and not any(_is_within(path, item) for item in orchestrator)
|
|
468
472
|
}
|
|
469
473
|
if snapshot.artifact_root == snapshot.root:
|
|
470
474
|
return []
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Mini JSON Schema validator for
|
|
1
|
+
"""Mini JSON Schema validator for versioned final-report data.json records.
|
|
2
2
|
|
|
3
3
|
This is a deliberately narrow JSON Schema implementation that supports
|
|
4
4
|
exactly the keywords used by ``schemas/final-report-v2.0.schema.json``:
|
|
5
5
|
|
|
6
6
|
type, required, properties, additionalProperties, enum, const, pattern,
|
|
7
7
|
minLength, minItems, maxItems, uniqueItems, items, minimum, maximum, $ref ($defs),
|
|
8
|
-
oneOf, allOf, if/then/else, contains, not
|
|
8
|
+
oneOf, allOf, if/then/else, contains, minContains, maxContains, not
|
|
9
9
|
|
|
10
10
|
We do NOT depend on the ``jsonschema`` PyPI package because its
|
|
11
11
|
dependency tree (``referencing``, ``rpds-py``) includes a Rust C
|
|
@@ -13,8 +13,8 @@ extension which we would have to vendor or ship pre-built per platform.
|
|
|
13
13
|
The ~250 lines below cover everything the final-report schema needs;
|
|
14
14
|
strict spec compliance is not a goal — strict validation of OUR schema is.
|
|
15
15
|
|
|
16
|
-
Error messages include the JSON pointer of the failing field so
|
|
17
|
-
|
|
16
|
+
Error messages include the JSON pointer of the failing field so report
|
|
17
|
+
assembly can return the defect to the input owner.
|
|
18
18
|
"""
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
@@ -23,9 +23,6 @@ import re
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
-
from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION
|
|
27
|
-
|
|
28
|
-
|
|
29
26
|
class SchemaError(ValueError):
|
|
30
27
|
"""Raised when a schema itself is malformed (e.g. a $ref points
|
|
31
28
|
nowhere). Distinct from a data validation failure.
|
|
@@ -204,10 +201,20 @@ class _Validator:
|
|
|
204
201
|
for i, value in enumerate(instance):
|
|
205
202
|
self.validate(value, items, path + (i,))
|
|
206
203
|
contains = schema.get("contains")
|
|
207
|
-
if contains is not None
|
|
208
|
-
self._matches(value, contains) for value in instance
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
if contains is not None:
|
|
205
|
+
matches = sum(self._matches(value, contains) for value in instance)
|
|
206
|
+
minimum = schema.get("minContains", 1)
|
|
207
|
+
maximum = schema.get("maxContains")
|
|
208
|
+
if matches < minimum:
|
|
209
|
+
self._err(
|
|
210
|
+
path,
|
|
211
|
+
f"array contains {matches} matching item(s), below minContains {minimum}",
|
|
212
|
+
)
|
|
213
|
+
if maximum is not None and matches > maximum:
|
|
214
|
+
self._err(
|
|
215
|
+
path,
|
|
216
|
+
f"array contains {matches} matching item(s), above maxContains {maximum}",
|
|
217
|
+
)
|
|
211
218
|
|
|
212
219
|
def _validate_conditional(self, instance: Any, schema: dict, path: tuple[str | int, ...]) -> None:
|
|
213
220
|
if_schema = schema.get("if")
|
|
@@ -263,6 +270,7 @@ def validate(data: Any, schema: dict) -> list[str]:
|
|
|
263
270
|
|
|
264
271
|
SCHEMA_FILENAMES = {
|
|
265
272
|
"2.0": "final-report-v2.0.schema.json",
|
|
273
|
+
"3.0": "final-report-v3.0.schema.json",
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
|
|
@@ -295,10 +303,11 @@ def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
|
|
|
295
303
|
|
|
296
304
|
|
|
297
305
|
def load_schema(schema_path: Path | None = None) -> dict:
|
|
298
|
-
"""Load the
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
"""Load the compatibility schema used by historical direct callers.
|
|
307
|
+
|
|
308
|
+
New write paths select contract 3.0 explicitly. Readers select from the
|
|
309
|
+
record's ``schemaVersion`` through :func:`load_schema_for_data`.
|
|
301
310
|
"""
|
|
302
311
|
if schema_path is not None:
|
|
303
312
|
return json.loads(Path(schema_path).read_text(encoding="utf-8"))
|
|
304
|
-
return load_schema_version(
|
|
313
|
+
return load_schema_version("2.0")
|
|
@@ -7,9 +7,15 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import sys
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from .convergence_store import write_json_atomic
|
|
13
|
+
from .final_report_schema import load_schema_version
|
|
14
|
+
from .report_narrative import parse_narrative, writer_owned_data
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
PREP_VERDICT_RE = re.compile(r"^P-Prep-S([1-9][0-9]*)-")
|
|
18
|
+
STEP_VERDICT_RE = re.compile(r"^P-Step-([1-9][0-9]*)\.")
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class CarryError(Exception):
|
|
@@ -290,6 +296,78 @@ def merge_carried_forward(
|
|
|
290
296
|
return cur
|
|
291
297
|
|
|
292
298
|
|
|
299
|
+
def _plan_item_stage(item: Mapping[str, Any]) -> int | None:
|
|
300
|
+
item_id = str(item.get("id") or "")
|
|
301
|
+
for pattern in (PREP_VERDICT_RE, STEP_VERDICT_RE):
|
|
302
|
+
match = pattern.match(item_id)
|
|
303
|
+
if match:
|
|
304
|
+
return int(match.group(1))
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _writer_stage_rows(data: Mapping[str, Any], *, snapshot: str) -> dict[int, dict]:
|
|
309
|
+
planning = _planning(dict(writer_owned_data(data)))
|
|
310
|
+
return _stage_rows(planning, snapshot=snapshot)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def merge_v3_plan_state(
|
|
314
|
+
prev: dict[str, Any],
|
|
315
|
+
narrative: dict[str, Any],
|
|
316
|
+
state: dict[str, Any],
|
|
317
|
+
*,
|
|
318
|
+
prev_seq: str,
|
|
319
|
+
carry_stages: set[int],
|
|
320
|
+
reverify_stages: set[int],
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""이전 판정 중 이월 스테이지 소유분만 v3 수렴 상태에 복사한다."""
|
|
323
|
+
prev_stages = _writer_stage_rows(prev, snapshot="prior")
|
|
324
|
+
cur_stages = _stage_rows(_planning(narrative), snapshot="current")
|
|
325
|
+
_validate_stage_sets(prev_stages, cur_stages, carry_stages, reverify_stages)
|
|
326
|
+
for stage in sorted(carry_stages):
|
|
327
|
+
current = cur_stages.get(stage)
|
|
328
|
+
if current is None:
|
|
329
|
+
raise CarryError(f"carry stage {stage} is missing from current narrative")
|
|
330
|
+
if _canonical(prev_stages[stage]) != _canonical(current):
|
|
331
|
+
raise CarryError(f"carry stage {stage} changed in current narrative")
|
|
332
|
+
|
|
333
|
+
prev_items = _indexed_rows(
|
|
334
|
+
_planning(prev).get("planBodyVerification", {}).get("planItems", []),
|
|
335
|
+
key="id",
|
|
336
|
+
label="prior planItems",
|
|
337
|
+
)
|
|
338
|
+
state_items = _indexed_rows(
|
|
339
|
+
state.get("planBodyVerification", {}).get("planItems", []),
|
|
340
|
+
key="id",
|
|
341
|
+
label="current state planItems",
|
|
342
|
+
)
|
|
343
|
+
for item_id, current in state_items.items():
|
|
344
|
+
stage = _plan_item_stage(current)
|
|
345
|
+
if stage not in carry_stages:
|
|
346
|
+
continue
|
|
347
|
+
previous = prev_items.get(item_id)
|
|
348
|
+
if previous is None:
|
|
349
|
+
raise CarryError(f"carry-owned plan item {item_id} is missing from prior report")
|
|
350
|
+
for field in ("subject", "sourceSection"):
|
|
351
|
+
if current.get(field) != previous.get(field):
|
|
352
|
+
raise CarryError(f"carry-owned plan item {item_id} changed {field}")
|
|
353
|
+
carried = copy.deepcopy(previous)
|
|
354
|
+
carried["carriedForwardFromSeq"] = str(prev_seq)
|
|
355
|
+
state_items[item_id] = carried
|
|
356
|
+
|
|
357
|
+
prior_carried_ids = {
|
|
358
|
+
item_id
|
|
359
|
+
for item_id, item in prev_items.items()
|
|
360
|
+
if _plan_item_stage(item) in carry_stages
|
|
361
|
+
}
|
|
362
|
+
missing = sorted(prior_carried_ids - set(state_items))
|
|
363
|
+
if missing:
|
|
364
|
+
raise CarryError(f"current state omitted carry-owned plan items: {missing}")
|
|
365
|
+
state["planBodyVerification"]["planItems"] = [
|
|
366
|
+
state_items[item_id] for item_id in sorted(state_items)
|
|
367
|
+
]
|
|
368
|
+
return state
|
|
369
|
+
|
|
370
|
+
|
|
293
371
|
def _parse_stage_csv(value: str | None, *, option: str) -> set[int] | None:
|
|
294
372
|
if value is None:
|
|
295
373
|
return None
|
|
@@ -317,33 +395,67 @@ def _load_data(path: str, *, option: str) -> dict:
|
|
|
317
395
|
def main(argv: list[str]) -> int:
|
|
318
396
|
ap = argparse.ArgumentParser(prog="okstra incremental-carry")
|
|
319
397
|
ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
|
|
320
|
-
ap.
|
|
398
|
+
current = ap.add_mutually_exclusive_group(required=True)
|
|
399
|
+
current.add_argument("--cur-data", help="historical v2 current data.json")
|
|
400
|
+
current.add_argument("--cur-narrative", help="contract v3 current narrative Markdown")
|
|
401
|
+
ap.add_argument("--state", help="contract v3 convergence-owned plan state")
|
|
321
402
|
ap.add_argument("--prev-seq", required=True, help="prior run seq, tagged onto carried items")
|
|
322
403
|
ap.add_argument("--carry-stages", default=None, help="comma-separated carried stage numbers")
|
|
323
404
|
ap.add_argument("--reverify-stages", default=None, help="comma-separated reverified stage numbers")
|
|
324
|
-
ap.add_argument("--out",
|
|
405
|
+
ap.add_argument("--out", help="historical v2 merged data.json path")
|
|
406
|
+
ap.add_argument("--out-state", help="contract v3 merged plan state path")
|
|
325
407
|
args = ap.parse_args(argv)
|
|
326
408
|
|
|
327
409
|
try:
|
|
328
410
|
prev = _load_data(args.prev_data, option="--prev-data")
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cur,
|
|
333
|
-
prev_seq=args.prev_seq,
|
|
334
|
-
carry_stages=_parse_stage_csv(args.carry_stages, option="--carry-stages"),
|
|
335
|
-
reverify_stages=_parse_stage_csv(
|
|
336
|
-
args.reverify_stages, option="--reverify-stages",
|
|
337
|
-
),
|
|
411
|
+
carry_stages = _parse_stage_csv(args.carry_stages, option="--carry-stages")
|
|
412
|
+
reverify_stages = _parse_stage_csv(
|
|
413
|
+
args.reverify_stages, option="--reverify-stages",
|
|
338
414
|
)
|
|
339
|
-
|
|
415
|
+
if args.cur_narrative:
|
|
416
|
+
if not args.state or not args.out_state or args.out:
|
|
417
|
+
raise CarryError(
|
|
418
|
+
"--cur-narrative requires --state and --out-state, and forbids --out"
|
|
419
|
+
)
|
|
420
|
+
if carry_stages is None or reverify_stages is None:
|
|
421
|
+
raise CarryError("contract v3 carry requires both stage sets")
|
|
422
|
+
try:
|
|
423
|
+
narrative = parse_narrative(
|
|
424
|
+
Path(args.cur_narrative).read_text(encoding="utf-8"),
|
|
425
|
+
load_schema_version("3.0"),
|
|
426
|
+
)
|
|
427
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
428
|
+
raise CarryError(f"--cur-narrative could not be read: {exc}") from exc
|
|
429
|
+
state = _load_data(args.state, option="--state")
|
|
430
|
+
merged = merge_v3_plan_state(
|
|
431
|
+
prev,
|
|
432
|
+
narrative,
|
|
433
|
+
state,
|
|
434
|
+
prev_seq=args.prev_seq,
|
|
435
|
+
carry_stages=carry_stages,
|
|
436
|
+
reverify_stages=reverify_stages,
|
|
437
|
+
)
|
|
438
|
+
output = Path(args.out_state)
|
|
439
|
+
else:
|
|
440
|
+
if not args.out or args.out_state or args.state:
|
|
441
|
+
raise CarryError(
|
|
442
|
+
"--cur-data requires --out, and forbids --state and --out-state"
|
|
443
|
+
)
|
|
444
|
+
cur = _load_data(args.cur_data, option="--cur-data")
|
|
445
|
+
merged = merge_carried_forward(
|
|
446
|
+
prev,
|
|
447
|
+
cur,
|
|
448
|
+
prev_seq=args.prev_seq,
|
|
449
|
+
carry_stages=carry_stages,
|
|
450
|
+
reverify_stages=reverify_stages,
|
|
451
|
+
)
|
|
452
|
+
output = Path(args.out)
|
|
453
|
+
except (CarryError, KeyError, TypeError) as exc:
|
|
340
454
|
print(f"carry refused: {exc}", file=sys.stderr)
|
|
341
455
|
return 1
|
|
342
456
|
|
|
343
457
|
try:
|
|
344
|
-
|
|
345
|
-
json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
346
|
-
)
|
|
458
|
+
write_json_atomic(output, merged)
|
|
347
459
|
except OSError as exc:
|
|
348
460
|
print(f"carry refused: --out could not be written: {exc}", file=sys.stderr)
|
|
349
461
|
return 1
|
|
@@ -354,7 +466,7 @@ def main(argv: list[str]) -> int:
|
|
|
354
466
|
.get("planItems", [])
|
|
355
467
|
if item.get("carriedForwardFromSeq") == str(args.prev_seq)
|
|
356
468
|
)
|
|
357
|
-
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {
|
|
469
|
+
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {output}")
|
|
358
470
|
return 0
|
|
359
471
|
|
|
360
472
|
|
|
@@ -207,7 +207,10 @@ def stages_for_clarification(data: dict, clarification_id: str) -> set[int]:
|
|
|
207
207
|
)
|
|
208
208
|
stages = _stages_blocked_on(planning.get("requirementCoverage"), clarification_id)
|
|
209
209
|
for item in plan_items if isinstance(plan_items, list) else []:
|
|
210
|
-
if isinstance(item, dict) and
|
|
210
|
+
if isinstance(item, dict) and (
|
|
211
|
+
item.get("clarificationId") == clarification_id
|
|
212
|
+
or clarification_id in (item.get("clarificationRefs") or [])
|
|
213
|
+
):
|
|
211
214
|
stages |= _plan_item_stages(item)
|
|
212
215
|
return stages
|
|
213
216
|
|
|
@@ -439,6 +439,10 @@ def _run_files(
|
|
|
439
439
|
"codex_worker_result": worker_results / f"codex-worker{suffixes['worker_results']}.md",
|
|
440
440
|
"antigravity_worker_result": worker_results / f"antigravity-worker{suffixes['worker_results']}.md",
|
|
441
441
|
"report_writer_worker_result": worker_results / f"report-writer-worker{suffixes['worker_results']}.md",
|
|
442
|
+
"report_writer_narrative": worker_results / f"report-writer-narrative{suffixes['worker_results']}.md",
|
|
443
|
+
"approval_decisions": run_state / f"approval-decisions-{task_type_segment}-{sequences['state']}.json",
|
|
444
|
+
"design_preparation": run_state / f"design-preparation-{task_type_segment}-{sequences['state']}.json",
|
|
445
|
+
"plan_body_verification": run_state / f"plan-body-verification-{task_type_segment}-{sequences['state']}.json",
|
|
442
446
|
"run_errors_log": run_logs / f"errors-{task_type_segment}-{sequences['state']}.jsonl",
|
|
443
447
|
"claude_worker_errors_sidecar": worker_results / f"claude-worker-errors{suffixes['worker_results']}.json",
|
|
444
448
|
"codex_worker_errors_sidecar": worker_results / f"codex-worker-errors{suffixes['worker_results']}.json",
|
|
@@ -570,6 +574,10 @@ def _absolute_worker_fields(paths: Mapping[str, Any]) -> dict[str, str]:
|
|
|
570
574
|
"CODEX_WORKER_RESULT_FILE": str(paths["codex_worker_result"]),
|
|
571
575
|
"ANTIGRAVITY_WORKER_RESULT_FILE": str(paths["antigravity_worker_result"]),
|
|
572
576
|
"REPORT_WRITER_WORKER_RESULT_FILE": str(paths["report_writer_worker_result"]),
|
|
577
|
+
"REPORT_WRITER_NARRATIVE_FILE": str(paths["report_writer_narrative"]),
|
|
578
|
+
"APPROVAL_DECISIONS_PATH": str(paths["approval_decisions"]),
|
|
579
|
+
"DESIGN_PREPARATION_PATH": str(paths["design_preparation"]),
|
|
580
|
+
"PLAN_BODY_VERIFICATION_STATE_PATH": str(paths["plan_body_verification"]),
|
|
573
581
|
"CLAUDE_WORKER_ERRORS_SIDECAR_PATH": str(paths["claude_worker_errors_sidecar"]),
|
|
574
582
|
"CODEX_WORKER_ERRORS_SIDECAR_PATH": str(paths["codex_worker_errors_sidecar"]),
|
|
575
583
|
"ANTIGRAVITY_WORKER_ERRORS_SIDECAR_PATH": str(paths["antigravity_worker_errors_sidecar"]),
|
|
@@ -670,6 +678,10 @@ def _relative_file_fields(project_root: Path, paths: Mapping[str, Any]) -> dict[
|
|
|
670
678
|
"CODEX_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["codex_worker_result"]),
|
|
671
679
|
"ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["antigravity_worker_result"]),
|
|
672
680
|
"REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH": _rel(project_root, paths["report_writer_worker_result"]),
|
|
681
|
+
"REPORT_WRITER_NARRATIVE_RELATIVE_PATH": _rel(project_root, paths["report_writer_narrative"]),
|
|
682
|
+
"APPROVAL_DECISIONS_RELATIVE_PATH": _rel(project_root, paths["approval_decisions"]),
|
|
683
|
+
"DESIGN_PREPARATION_RELATIVE_PATH": _rel(project_root, paths["design_preparation"]),
|
|
684
|
+
"PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH": _rel(project_root, paths["plan_body_verification"]),
|
|
673
685
|
"RUN_ERRORS_LOG_RELATIVE_PATH": _rel(project_root, paths["run_errors_log"]),
|
|
674
686
|
"CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH": _rel(project_root, paths["claude_worker_errors_sidecar"]),
|
|
675
687
|
"CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH": _rel(project_root, paths["codex_worker_errors_sidecar"]),
|
|
@@ -562,6 +562,10 @@ def compute_run_paths(
|
|
|
562
562
|
codex_worker_result = worker_results / f"codex-worker{suffixes['worker_results']}.md"
|
|
563
563
|
antigravity_worker_result = worker_results / f"antigravity-worker{suffixes['worker_results']}.md"
|
|
564
564
|
report_writer_worker_result = worker_results / f"report-writer-worker{suffixes['worker_results']}.md"
|
|
565
|
+
report_writer_narrative = worker_results / f"report-writer-narrative{suffixes['worker_results']}.md"
|
|
566
|
+
approval_decisions = run_state / f"approval-decisions-{task_type_segment}-{seqs['state']}.json"
|
|
567
|
+
design_preparation = run_state / f"design-preparation-{task_type_segment}-{seqs['state']}.json"
|
|
568
|
+
plan_body_verification = run_state / f"plan-body-verification-{task_type_segment}-{seqs['state']}.json"
|
|
565
569
|
|
|
566
570
|
run_errors_log = run_logs / f"errors-{task_type_segment}-{seqs['state']}.jsonl"
|
|
567
571
|
runtime_home = okstra_home()
|
|
@@ -667,6 +671,10 @@ def compute_run_paths(
|
|
|
667
671
|
"CODEX_WORKER_RESULT_FILE": str(codex_worker_result),
|
|
668
672
|
"ANTIGRAVITY_WORKER_RESULT_FILE": str(antigravity_worker_result),
|
|
669
673
|
"REPORT_WRITER_WORKER_RESULT_FILE": str(report_writer_worker_result),
|
|
674
|
+
"REPORT_WRITER_NARRATIVE_FILE": str(report_writer_narrative),
|
|
675
|
+
"APPROVAL_DECISIONS_PATH": str(approval_decisions),
|
|
676
|
+
"DESIGN_PREPARATION_PATH": str(design_preparation),
|
|
677
|
+
"PLAN_BODY_VERIFICATION_STATE_PATH": str(plan_body_verification),
|
|
670
678
|
"RUN_ERRORS_LOG_PATH": str(run_errors_log),
|
|
671
679
|
"CLAUDE_WORKER_ERRORS_SIDECAR_PATH": str(claude_worker_errors_sidecar),
|
|
672
680
|
"CODEX_WORKER_ERRORS_SIDECAR_PATH": str(codex_worker_errors_sidecar),
|
|
@@ -737,6 +745,10 @@ def compute_run_paths(
|
|
|
737
745
|
("CODEX_WORKER_RESULT_RELATIVE_PATH", codex_worker_result),
|
|
738
746
|
("ANTIGRAVITY_WORKER_RESULT_RELATIVE_PATH", antigravity_worker_result),
|
|
739
747
|
("REPORT_WRITER_WORKER_RESULT_RELATIVE_PATH", report_writer_worker_result),
|
|
748
|
+
("REPORT_WRITER_NARRATIVE_RELATIVE_PATH", report_writer_narrative),
|
|
749
|
+
("APPROVAL_DECISIONS_RELATIVE_PATH", approval_decisions),
|
|
750
|
+
("DESIGN_PREPARATION_RELATIVE_PATH", design_preparation),
|
|
751
|
+
("PLAN_BODY_VERIFICATION_STATE_RELATIVE_PATH", plan_body_verification),
|
|
740
752
|
("RUN_ERRORS_LOG_RELATIVE_PATH", run_errors_log),
|
|
741
753
|
("CLAUDE_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", claude_worker_errors_sidecar),
|
|
742
754
|
("CODEX_WORKER_ERRORS_SIDECAR_RELATIVE_PATH", codex_worker_errors_sidecar),
|