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.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.mjs +14 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/incremental-carry.mjs +9 -8
  5. package/dist/commands/execute/incremental-carry.mjs.map +1 -1
  6. package/dist/commands/execute/plan-verify.mjs +3 -1
  7. package/dist/commands/execute/plan-verify.mjs.map +1 -1
  8. package/dist/commands/report/approval-decision.d.mts +1 -0
  9. package/dist/commands/report/approval-decision.mjs +21 -0
  10. package/dist/commands/report/approval-decision.mjs.map +1 -0
  11. package/dist/commands/report/design-snapshot.d.mts +1 -0
  12. package/dist/commands/report/design-snapshot.mjs +19 -0
  13. package/dist/commands/report/design-snapshot.mjs.map +1 -0
  14. package/docs/architecture/storage-model.md +1 -1
  15. package/docs/architecture.md +10 -10
  16. package/docs/cli.md +11 -8
  17. package/docs/project-structure-overview.md +15 -6
  18. package/docs/task-process/implementation-planning.md +2 -2
  19. package/package.json +1 -1
  20. package/runtime/BUILD.json +2 -2
  21. package/runtime/agents/workers/report-writer-worker.md +15 -164
  22. package/runtime/prompts/launch.template.md +6 -5
  23. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  24. package/runtime/prompts/lead/convergence.md +2 -2
  25. package/runtime/prompts/lead/okstra-lead-contract.md +19 -18
  26. package/runtime/prompts/lead/plan-body-verification.md +43 -19
  27. package/runtime/prompts/lead/report-writer.md +64 -423
  28. package/runtime/prompts/lead/team-contract.md +1 -1
  29. package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
  30. package/runtime/prompts/profiles/_common-contract.md +3 -3
  31. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  32. package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
  33. package/runtime/prompts/profiles/error-analysis.md +1 -1
  34. package/runtime/prompts/profiles/feature-analysis.md +1 -1
  35. package/runtime/prompts/profiles/implementation-planning.md +13 -11
  36. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  37. package/runtime/prompts/profiles/project-analysis.md +1 -1
  38. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  39. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +2 -1
  40. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  41. package/runtime/python/okstra_ctl/agent_activity.py +23 -3
  42. package/runtime/python/okstra_ctl/agent_prompt_cli.py +6 -6
  43. package/runtime/python/okstra_ctl/analysis_packet.py +43 -2
  44. package/runtime/python/okstra_ctl/approval_decisions.py +327 -0
  45. package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
  46. package/runtime/python/okstra_ctl/dispatch_core.py +84 -13
  47. package/runtime/python/okstra_ctl/dispatch_state.py +29 -4
  48. package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
  49. package/runtime/python/okstra_ctl/final_report_schema.py +24 -15
  50. package/runtime/python/okstra_ctl/incremental_carry.py +128 -16
  51. package/runtime/python/okstra_ctl/incremental_scope.py +4 -1
  52. package/runtime/python/okstra_ctl/path_hints.py +12 -0
  53. package/runtime/python/okstra_ctl/paths.py +12 -0
  54. package/runtime/python/okstra_ctl/plan_items_cli.py +113 -16
  55. package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
  56. package/runtime/python/okstra_ctl/render.py +48 -1
  57. package/runtime/python/okstra_ctl/render_final_report.py +26 -20
  58. package/runtime/python/okstra_ctl/report_assembly.py +354 -0
  59. package/runtime/python/okstra_ctl/report_contract.py +2 -1
  60. package/runtime/python/okstra_ctl/report_finalize.py +60 -22
  61. package/runtime/python/okstra_ctl/report_inputs.py +72 -0
  62. package/runtime/python/okstra_ctl/report_markdown.py +69 -8
  63. package/runtime/python/okstra_ctl/report_narrative.py +319 -0
  64. package/runtime/python/okstra_ctl/report_projections.py +265 -0
  65. package/runtime/python/okstra_ctl/run.py +25 -9
  66. package/runtime/python/okstra_ctl/schema_excerpt.py +94 -14
  67. package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -4
  68. package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
  69. package/runtime/python/okstra_ctl/stage_map.py +70 -22
  70. package/runtime/python/okstra_ctl/team.py +1 -1
  71. package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
  72. package/runtime/python/okstra_ctl/worker_prompt_body.py +35 -0
  73. package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
  74. package/runtime/schemas/final-report-v3.0.schema.json +10210 -0
  75. package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
  76. package/runtime/templates/report-writer-prompt-preamble.md +15 -21
  77. package/runtime/templates/reports/html/macros/forms.html +6 -4
  78. 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
- idle_timeout_seconds: int
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
- str(self.idle_timeout_seconds),
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 not in orchestrator
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 not in orchestrator
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 the final-report data.json.
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 the
17
- report-writer can fix the exact data.json cell.
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 and not any(
208
- self._matches(value, contains) for value in instance
209
- ):
210
- self._err(path, f"array does not contain any item matching {self._summarise(contains)}")
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 final-report schema. If ``schema_path`` is None, locate
299
- ``schemas/final-report-v2.0.schema.json`` relative to this file's
300
- repo root.
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(CURRENT_REPORT_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.add_argument("--cur-data", required=True, help="current incremental re-run data.json")
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", required=True, help="path to write the merged data.json")
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
- cur = _load_data(args.cur_data, option="--cur-data")
330
- merged = merge_carried_forward(
331
- prev,
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
- except CarryError as exc:
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
- Path(args.out).write_text(
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} -> {args.out}")
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 item.get("clarificationId") == clarification_id:
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),