okstra 0.102.2 → 0.102.3

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 (161) hide show
  1. package/README.kr.md +5 -5
  2. package/README.md +5 -5
  3. package/docs/kr/architecture/storage-model.md +1 -1
  4. package/docs/kr/architecture.md +7 -11
  5. package/docs/kr/cli.md +4 -4
  6. package/docs/kr/container.md +1 -1
  7. package/docs/kr/performance-improvement-plan-v2.md +2 -2
  8. package/docs/project-structure-overview.md +2 -2
  9. package/package.json +7 -2
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-antigravity-exec.sh +1 -2
  12. package/runtime/bin/okstra-codex-exec.sh +1 -2
  13. package/runtime/bin/okstra-render-final-report.py +2 -9
  14. package/runtime/bin/okstra-spawn-followups.py +4 -6
  15. package/runtime/bin/okstra-trace-cleanup.sh +24 -4
  16. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  17. package/runtime/prompts/lead/report-writer.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +9 -11
  19. package/runtime/python/okstra_ctl/codex_dispatch.py +4 -12
  20. package/runtime/python/okstra_ctl/consumers.py +35 -0
  21. package/runtime/python/okstra_ctl/container.py +4 -6
  22. package/runtime/python/okstra_ctl/dispatch_core.py +4 -12
  23. package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
  24. package/runtime/python/okstra_ctl/handoff.py +2 -1
  25. package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
  26. package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
  27. package/runtime/python/okstra_ctl/report_views.py +7 -4
  28. package/runtime/python/okstra_ctl/run.py +3 -10
  29. package/runtime/python/okstra_ctl/stage_targets.py +18 -0
  30. package/runtime/python/okstra_ctl/wizard.py +24 -25
  31. package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
  32. package/runtime/python/okstra_token_usage/report.py +2 -5
  33. package/runtime/validators/validate-report-views.py +2 -1
  34. package/runtime/validators/validate-run.py +1 -11
  35. package/src/commands/lifecycle/install.mjs +5 -5
  36. package/src/commands/lifecycle/uninstall.mjs +4 -4
  37. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
  38. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
  39. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
  40. package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
  41. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
  42. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
  43. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
  44. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
  45. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
  46. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
  47. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
  48. package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
  49. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
  50. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
  51. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
  52. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
  53. package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
  54. package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
  55. package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
  56. package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
  57. package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
  58. package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
  59. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
  60. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
  61. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
  62. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
  63. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
  64. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
  65. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
  66. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
  67. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
  68. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
  69. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
  70. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
  71. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
  72. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
  73. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
  74. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
  75. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
  76. package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
  77. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
  78. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
  79. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
  80. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
  81. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
  82. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
  83. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
  84. package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
  85. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
  86. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
  87. package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
  88. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
  89. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
  90. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
  91. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
  92. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
  93. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
  94. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
  95. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
  96. package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
  97. package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
  98. package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
  99. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
  100. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
  101. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
  102. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
  103. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
  104. package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
  105. package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
  106. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
  107. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
  108. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
  109. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
  110. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
  111. package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
  112. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
  113. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
  114. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
  115. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
  116. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
  117. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
  118. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
  119. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
  120. package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
  121. package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
  122. package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
  123. package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
  124. package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
  125. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
  126. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
  127. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
  128. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
  129. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
  130. package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
  131. package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
  132. package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
  133. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
  134. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
  135. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
  136. package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
  137. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
  138. package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
  139. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
  140. package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
  141. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
  142. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
  143. package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
  144. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
  145. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
  146. package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
  147. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
  148. package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
  149. package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
  150. package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
  151. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
  152. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
  153. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
  154. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
  155. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
  156. package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
  157. package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
  158. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
  159. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
  160. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
  161. package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
@@ -18,6 +18,10 @@ from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
  from typing import Any, Mapping, Sequence
20
20
 
21
+ from .final_report_paths import (
22
+ final_report_data_path as _final_report_data_path,
23
+ final_report_markdown_path as _final_report_markdown_path,
24
+ )
21
25
  from .lead_events import LeadEvent, append_lead_event
22
26
  from .wrapper_status import status_path_for_prompt
23
27
  from .paths import task_dir
@@ -1450,18 +1454,6 @@ def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
1450
1454
  return "worker dispatch failed"
1451
1455
 
1452
1456
 
1453
- def _final_report_data_path(markdown_path: Path) -> Path:
1454
- if markdown_path.name.endswith(".md"):
1455
- return markdown_path.with_name(markdown_path.name[:-3] + ".data.json")
1456
- return markdown_path.with_suffix(".data.json")
1457
-
1458
-
1459
- def _final_report_markdown_path(data_json_path: Path) -> Path:
1460
- if data_json_path.name.endswith(".data.json"):
1461
- return data_json_path.with_name(data_json_path.name[:-10] + ".md")
1462
- return data_json_path.with_suffix(".md")
1463
-
1464
-
1465
1457
  def _replace_report_writer_model_line(
1466
1458
  prompt_path: Path,
1467
1459
  model_execution_value: str,
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  import re
12
+ from dataclasses import dataclass
12
13
  from pathlib import Path
13
14
  from typing import Any, Dict, List, Optional
14
15
 
@@ -17,6 +18,17 @@ from .run_context import consumers_mutex
17
18
  CONSUMERS_FILENAME = "consumers.jsonl"
18
19
 
19
20
 
21
+ @dataclass(frozen=True)
22
+ class StageConsumerState:
23
+ rows: List[Dict[str, Any]]
24
+ done_rows: List[Dict[str, Any]]
25
+ done_by_stage: Dict[int, Dict[str, Any]]
26
+ done_stages: set[int]
27
+ started_stages: set[int]
28
+ verified_accepted_stages: set[int]
29
+ pr_covered_stages: set[int]
30
+
31
+
20
32
  def _path(plan_run_root: Path) -> Path:
21
33
  return plan_run_root / CONSUMERS_FILENAME
22
34
 
@@ -50,6 +62,29 @@ def latest_done_by_stage(rows: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]
50
62
  return out
51
63
 
52
64
 
65
+ def read_stage_consumer_state(
66
+ plan_run_root: Path, *, recover_from_carry: bool = False,
67
+ ) -> StageConsumerState:
68
+ if recover_from_carry:
69
+ backfill_done_from_carry(plan_run_root)
70
+ rows = read_consumers(plan_run_root)
71
+ done_rows = [r for r in rows if r.get("status") == "done"]
72
+ done_by_stage = latest_done_by_stage(rows)
73
+ started = {
74
+ r["stage"] for r in rows
75
+ if r.get("status") == "started" and isinstance(r.get("stage"), int)
76
+ }
77
+ return StageConsumerState(
78
+ rows=rows,
79
+ done_rows=done_rows,
80
+ done_by_stage=done_by_stage,
81
+ done_stages=set(done_by_stage.keys()),
82
+ started_stages=started,
83
+ verified_accepted_stages=verified_accepted_stages(rows),
84
+ pr_covered_stages=pr_covered_stages(rows),
85
+ )
86
+
87
+
53
88
  def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
54
89
  status: str, force_reappend: bool = False,
55
90
  **fields: Any) -> None:
@@ -506,7 +506,7 @@ def _split_task_key(task_key: str) -> tuple[str, str, str]:
506
506
  def _resolve_up_inputs(project_root: Path, task_key: str) -> dict:
507
507
  """task-key 로 up 에 필요한 입력(worktree/stage_map/done_rows/anchor)을 모은다."""
508
508
  from . import worktree_registry as _reg
509
- from .consumers import read_consumers, backfill_done_from_carry
509
+ from .consumers import read_stage_consumer_state
510
510
  from .plan_run_root import resolve_plan_run_root_by_task_key
511
511
 
512
512
  project_id, group, task_id = _split_task_key(task_key)
@@ -517,11 +517,9 @@ def _resolve_up_inputs(project_root: Path, task_key: str) -> dict:
517
517
  "없습니다. 먼저 해당 task 의 phase 를 한 번 실행해 worktree 를 만드세요.")
518
518
  plan = resolve_plan_run_root_by_task_key(
519
519
  project_root=project_root, task_group=group, task_id=task_id)
520
- # carry sidecar 가 stage 완료의 SSOT 다 — final-verification 과 동일하게 누락된
521
- # done 행을 carry 에서 복구한 뒤 읽어, 두 경로가 같은 디스크 상태에서 갈라지지
522
- # 않게 한다(crash/부분쓰기로 consumers.jsonl done 행만 유실된 경우 대비).
523
- backfill_done_from_carry(plan.run_root)
524
- done_rows = [r for r in read_consumers(plan.run_root) if r.get("status") == "done"]
520
+ done_rows = read_stage_consumer_state(
521
+ plan.run_root, recover_from_carry=True,
522
+ ).done_rows
525
523
  stage_map = _plan_stage_map(plan.approved_plan_path)
526
524
  anchor = _reg.get_implementation_base(project_id, group, task_id) or ""
527
525
  return {
@@ -11,6 +11,10 @@ from pathlib import Path
11
11
  from typing import Any, Mapping, Sequence
12
12
 
13
13
  from . import tmux
14
+ from .final_report_paths import (
15
+ final_report_data_path as _final_report_data_path,
16
+ final_report_markdown_path as _final_report_markdown_path,
17
+ )
14
18
  from .lead_events import LeadEvent, append_lead_event
15
19
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
16
20
  from .wrapper_status import read_wrapper_status, status_path_for_prompt
@@ -823,18 +827,6 @@ def _completion_paths(worker_id: str, result_path: Path, manifest: Mapping[str,
823
827
  return (data_json, _final_report_markdown_path(data_json), result_path)
824
828
 
825
829
 
826
- def _final_report_data_path(markdown_path: Path) -> Path:
827
- if markdown_path.name.endswith(".md"):
828
- return markdown_path.with_name(markdown_path.name[:-3] + ".data.json")
829
- return markdown_path.with_suffix(".data.json")
830
-
831
-
832
- def _final_report_markdown_path(data_json_path: Path) -> Path:
833
- if data_json_path.name.endswith(".data.json"):
834
- return data_json_path.with_name(data_json_path.name[:-10] + ".md")
835
- return data_json_path.with_suffix(".md")
836
-
837
-
838
830
  def _run_dir(plan: DispatchPlan) -> Path:
839
831
  value = _string_value(plan.manifest.get("runDirectoryPath"))
840
832
  return _resolve_project_path(plan.project_root, value) if value else plan.team_state_path.parent.parent
@@ -0,0 +1,24 @@
1
+ """Path helpers for the final-report markdown/data.json pair."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+
7
+ DATA_JSON_SUFFIX = ".data.json"
8
+ MARKDOWN_SUFFIX = ".md"
9
+
10
+
11
+ def final_report_data_path(report_path: Path) -> Path:
12
+ """Return the data.json sibling for a final-report markdown path."""
13
+ name = report_path.name
14
+ if name.endswith(MARKDOWN_SUFFIX):
15
+ return report_path.with_name(name[: -len(MARKDOWN_SUFFIX)] + DATA_JSON_SUFFIX)
16
+ return report_path.with_suffix(DATA_JSON_SUFFIX)
17
+
18
+
19
+ def final_report_markdown_path(data_path: Path) -> Path:
20
+ """Return the markdown sibling for a final-report data.json path."""
21
+ name = data_path.name
22
+ if name.endswith(DATA_JSON_SUFFIX):
23
+ return data_path.with_name(name[: -len(DATA_JSON_SUFFIX)] + MARKDOWN_SUFFIX)
24
+ return data_path.with_suffix(MARKDOWN_SUFFIX)
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from typing import Any, Callable, Dict, List, Optional, Tuple
14
14
 
15
15
  from . import consumers, worktree_registry
16
+ from .final_report_paths import final_report_markdown_path
16
17
  from .worktree import (compute_branch_name, compute_worktree_path,
17
18
  main_worktree_path, is_dirty_excluding_okstra,
18
19
  nested_worktree_excludes, is_ancestor, merge_branch,
@@ -91,7 +92,7 @@ def latest_whole_task_fv_accepted(project_root, project_id: str,
91
92
  if ((data.get("header") or {}).get("taskType") == "final-verification"
92
93
  and data.get("verificationScope") == "whole-task"
93
94
  and token == "accepted"):
94
- md = dj.with_name(dj.name[:-len(".data.json")] + ".md")
95
+ md = final_report_markdown_path(dj)
95
96
  return str(md if md.is_file() else dj)
96
97
  return ""
97
98
 
@@ -66,16 +66,14 @@ def select_and_provision_implementation_stage(
66
66
  """
67
67
  del task_key # The registry uses the split identity fields.
68
68
 
69
- from .consumers import read_consumers, backfill_done_from_carry
69
+ from .consumers import backfill_done_from_carry, read_stage_consumer_state
70
70
  from . import worktree as _worktree
71
71
  from . import worktree_registry as _reg
72
72
 
73
73
  plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
74
74
  backfill_done_from_carry(plan_run_root)
75
75
  auto_reconcile_best_effort(inp, plan_run_root)
76
- consumed = read_consumers(plan_run_root)
77
- done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
78
- started_stages = {r["stage"] for r in consumed if r.get("status") == "started"}
76
+ consumers = read_stage_consumer_state(plan_run_root)
79
77
  reserved_stages = _reg.list_active_stage_numbers(
80
78
  inp.project_id,
81
79
  inp.task_group,
@@ -83,18 +81,17 @@ def select_and_provision_implementation_stage(
83
81
  )
84
82
 
85
83
  try:
86
- batch = stage_targets.resolve_effective_stages(
84
+ selected = stage_targets.resolve_implementation_stage(
87
85
  ctx_stage_map,
88
- done_stages,
86
+ consumers.done_stages,
89
87
  inp.stage,
90
- started_stages=started_stages,
88
+ started_stages=consumers.started_stages,
91
89
  reserved_stages=reserved_stages,
92
90
  )
93
91
  except stage_targets.StageTargetError as exc:
94
92
  raise _as_implementation_stage_error(exc) from exc
95
93
 
96
- selected = batch[0]
97
- concurrent_stages = sorted(reserved_stages - done_stages - {selected})
94
+ concurrent_stages = sorted(reserved_stages - consumers.done_stages - {selected})
98
95
 
99
96
  if executor_worktree_status.startswith("skipped"):
100
97
  head = _git_out(inp.project_root, "rev-parse", "HEAD")
@@ -133,11 +130,10 @@ def select_and_provision_implementation_stage(
133
130
  selected_stage = next(
134
131
  s for s in ctx_stage_map if s["stage_number"] == selected
135
132
  )
136
- consumer_done_rows = [r for r in consumed if r.get("status") == "done"]
137
133
  try:
138
134
  stage_base = stage_targets.resolve_stage_base_commit(
139
135
  selected_stage,
140
- consumer_done_rows,
136
+ consumers.done_rows,
141
137
  anchor_base_commit=anchor,
142
138
  candidate_base=head_sha,
143
139
  project_root=Path(task_worktree_path),
@@ -0,0 +1,14 @@
1
+ """Path helpers for generated final-report view artifacts."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+
7
+ def html_view_path(report_path: Path) -> Path:
8
+ """Return the self-contained HTML view path for a final-report markdown."""
9
+ return report_path.with_suffix(".html")
10
+
11
+
12
+ def user_responses_dir_for_report(report_path: Path) -> Path:
13
+ """Return the sidecar directory used by the report's exported responses."""
14
+ return report_path.parent.parent / "user-responses"
@@ -28,6 +28,9 @@ from dataclasses import dataclass
28
28
  from pathlib import Path
29
29
  from typing import Iterable, Optional
30
30
 
31
+ from .final_report_paths import final_report_data_path
32
+ from .report_view_artifacts import html_view_path, user_responses_dir_for_report
33
+
31
34
 
32
35
  def source_digest(src_md: str) -> str:
33
36
  """Stable identifier for a final-report markdown body. The HTML
@@ -904,7 +907,7 @@ def plan_approval_context(
904
907
  validator 가 같은 판정을 공유한다. recommendedOption 이 비거나 후보에 없으면
905
908
  첫 후보로 폴백한다 — 항상 정확히 한 옵션이 selected 가 되어 브라우저 자동선택분의
906
909
  묵시 Export 를 차단한다."""
907
- data_path = src_md_path.with_name(src_md_path.stem + ".data.json")
910
+ data_path = final_report_data_path(src_md_path)
908
911
  if not data_path.is_file():
909
912
  return None
910
913
  try:
@@ -1020,14 +1023,14 @@ def render_html_view(
1020
1023
  css: str,
1021
1024
  js: str,
1022
1025
  ) -> Path | None:
1023
- """Write ``<stem>.html`` next to ``src_md_path`` and return its path,
1026
+ """Write the HTML view artifact for ``src_md_path`` and return its path,
1024
1027
  or return ``None`` when the report needs no interactive view — §1
1025
1028
  clarification rows 도 없고 Plan Approval 위젯 대상도 아닐 때
1026
1029
  (``report_has_clarification_items`` / ``plan_approval_context``).
1027
1030
  Idempotent — overwrites an existing html sibling, and removes a stale
1028
1031
  one when the report no longer needs a view."""
1029
1032
  src_text = src_md_path.read_text(encoding="utf-8")
1030
- html_path = src_md_path.with_name(src_md_path.stem + ".html")
1033
+ html_path = html_view_path(src_md_path)
1031
1034
  # Fail-closed on §1 heading drift: when a `## 1. Clarification Items`
1032
1035
  # heading exists but its strict form does not parse,
1033
1036
  # `report_has_clarification_items` returns False (lenient parse), so a
@@ -1052,5 +1055,5 @@ def render_html_view(
1052
1055
  html_path.write_text(html_text, encoding="utf-8")
1053
1056
  # 사용자 확인(폼/승인)이 필요한 보고서 — Export 파일의 저장 위치를 미리
1054
1057
  # 만들어 사용자가 디렉토리를 만들 필요가 없게 한다 (reports/ 의 sibling).
1055
- (src_md_path.parent.parent / "user-responses").mkdir(parents=True, exist_ok=True)
1058
+ user_responses_dir_for_report(src_md_path).mkdir(parents=True, exist_ok=True)
1056
1059
  return html_path
@@ -43,6 +43,7 @@ from .material import (
43
43
  resolve_related_tasks,
44
44
  )
45
45
  from .final_report_schema import load_schema
46
+ from .final_report_paths import final_report_data_path as _final_report_data_path
46
47
  from .lead_events import LeadEvent, append_lead_event
47
48
  from .lead_runtime import ALLOWED_LEAD_RUNTIMES
48
49
  from .models import ModelAssignment, resolve_model_metadata
@@ -146,13 +147,6 @@ def _extract_frontmatter_block(body: str) -> str | None:
146
147
  return m.group(1) if m else None
147
148
 
148
149
 
149
- def _final_report_data_path(report_path: Path) -> Path:
150
- name = report_path.name
151
- if name.endswith(".md"):
152
- return report_path.with_name(name[:-3] + ".data.json")
153
- return report_path.with_suffix(".data.json")
154
-
155
-
156
150
  def _load_final_report_data_if_present(report_path: Path) -> tuple[Path, dict] | None:
157
151
  data_path = _final_report_data_path(report_path)
158
152
  if not data_path.is_file():
@@ -1514,7 +1508,7 @@ def _reserve_final_verification_target(
1514
1508
  """final-verification 의 검증 target 을 registry/consumers/git 에서
1515
1509
  해소하고 gate 를 강제한다. 위반 시 PrepareError. 결과를 ctx 의
1516
1510
  VERIFICATION_* 키로 주입한다."""
1517
- from .consumers import read_consumers, backfill_done_from_carry
1511
+ from .consumers import backfill_done_from_carry, read_stage_consumer_state
1518
1512
  from . import worktree_registry as _reg
1519
1513
 
1520
1514
  plan_run_root = plan_run_root_from_approved_plan(inp.approved_plan_path)
@@ -1522,8 +1516,7 @@ def _reserve_final_verification_target(
1522
1516
  # rows before the whole-task gate checks every stage.
1523
1517
  backfill_done_from_carry(plan_run_root)
1524
1518
  _auto_reconcile_best_effort(inp, plan_run_root)
1525
- done_rows = [r for r in read_consumers(plan_run_root)
1526
- if r.get("status") == "done"]
1519
+ done_rows = read_stage_consumer_state(plan_run_root).done_rows
1527
1520
 
1528
1521
  if inp.stage and inp.stage != "auto":
1529
1522
  n = int(inp.stage)
@@ -98,6 +98,24 @@ def resolve_effective_stages(
98
98
  return batch
99
99
 
100
100
 
101
+ def resolve_implementation_stage(
102
+ stages: list[dict[str, Any]],
103
+ done_stages: set[int],
104
+ requested: str,
105
+ *,
106
+ started_stages: set[int] | None = None,
107
+ reserved_stages: set[int] | None = None,
108
+ ) -> int:
109
+ """Return the single Stage Map stage selected for one implementation run."""
110
+ return resolve_effective_stages(
111
+ stages,
112
+ done_stages,
113
+ requested,
114
+ started_stages=started_stages,
115
+ reserved_stages=reserved_stages,
116
+ )[0]
117
+
118
+
101
119
  def order_stage_closure(
102
120
  stages, selected: set[int], done: set[int],
103
121
  ) -> list[int]:
@@ -34,6 +34,7 @@ from okstra_ctl.models import (
34
34
  resolve_model_metadata,
35
35
  )
36
36
  from okstra_ctl.clarification_items import scan_approval_gate, user_response_sidecars
37
+ from okstra_ctl.final_report_paths import final_report_data_path
37
38
  from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
38
39
  from okstra_ctl.run import (
39
40
  APPROVED_FRONTMATTER_PATTERN,
@@ -60,6 +61,12 @@ from okstra_ctl.workers import (
60
61
  validate_workers_against_profile,
61
62
  )
62
63
  from okstra_ctl.workflow import PHASE_SEQUENCE
64
+ from okstra_ctl.wizard_stage_intent import (
65
+ WHOLE_TASK_STAGE,
66
+ WizardStageIntentError,
67
+ resolve_wizard_stage_intent,
68
+ wizard_stage_confirmation_label,
69
+ )
63
70
  from okstra_ctl import fix_cycles, worktree_registry
64
71
  from okstra_ctl.worktree import (
65
72
  compute_worktree_path,
@@ -607,7 +614,7 @@ def _find_html_approval_sidecar(
607
614
  def _validate_sidecar_option(plan_path: Path, option_name: str, errors_t: dict) -> None:
608
615
  """sidecar 의 옵션 이름이 plan data.json 의 optionCandidates 에 있는지
609
616
  검증한다. 없으면 유효 후보를 나열하며 거부한다 (fail-closed)."""
610
- data_path = plan_path.with_name(plan_path.name[: -len(".md")] + ".data.json")
617
+ data_path = final_report_data_path(plan_path)
611
618
  candidates: list[str] = []
612
619
  if data_path.is_file():
613
620
  try:
@@ -1652,7 +1659,6 @@ _REUSE_LAST_TOKEN = "__reuse_last__"
1652
1659
  _SIBLINGS_TOKEN = "__siblings__"
1653
1660
  _LATEST_REPORT_TOKEN = "__latest_report__"
1654
1661
  _PROJECT_DEFAULT_TOKEN = "__project_default__"
1655
- WHOLE_TASK_STAGE = "__whole_task__"
1656
1662
  ALL_STAGES = "__all_stages__"
1657
1663
 
1658
1664
 
@@ -3488,21 +3494,14 @@ def render_args(state: WizardState) -> dict[str, str]:
3488
3494
  if state.reuse_worktree or state.task_type == "final-verification"
3489
3495
  else state.base_ref
3490
3496
  )
3491
- chain_stages = ""
3492
- if state.task_type == "implementation":
3493
- stage = state.selected_stage or "auto"
3494
- chain_stages = state.selected_stages or stage
3495
- elif state.task_type == "final-verification":
3496
- if state.selected_stage == WHOLE_TASK_STAGE:
3497
- stage = "" # prepare 가 빈 stage 를 whole-task 로 해석
3498
- elif not state.selected_stage or state.selected_stage == "auto":
3499
- raise WizardError(
3500
- "final-verification requires an explicit stage number"
3501
- )
3502
- else:
3503
- stage = state.selected_stage
3504
- else:
3505
- stage = ""
3497
+ try:
3498
+ stage_intent = resolve_wizard_stage_intent(
3499
+ task_type=state.task_type,
3500
+ selected_stage=state.selected_stage,
3501
+ selected_stages=state.selected_stages,
3502
+ )
3503
+ except WizardStageIntentError as exc:
3504
+ raise WizardError(str(exc)) from exc
3506
3505
  pr_template = (
3507
3506
  state.pr_template_path
3508
3507
  if state.task_type == "release-handoff"
@@ -3518,9 +3517,9 @@ def render_args(state: WizardState) -> dict[str, str]:
3518
3517
  "executor": state.executor,
3519
3518
  "critic": state.critic,
3520
3519
  "approved-plan": state.approved_plan_path,
3521
- "stage": stage,
3520
+ "stage": stage_intent.stage,
3522
3521
  "stages": state.handoff_stages,
3523
- "chain-stages": chain_stages,
3522
+ "chain-stages": stage_intent.chain_stages,
3524
3523
  "base-ref": base_ref,
3525
3524
  "workers": workers,
3526
3525
  "directive": state.directive,
@@ -3581,12 +3580,12 @@ def confirmation_block(state: WizardState) -> str:
3581
3580
  lines.append(f" critic : {state.critic or '(off)'}")
3582
3581
  if state.task_type in _STAGE_SCOPED_TASK_TYPES:
3583
3582
  lines.append(f" approved-plan : {state.approved_plan_path}")
3584
- stage = (
3585
- _msg(state.workspace_root, "confirmation", "stage_whole_task")
3586
- if state.selected_stage == WHOLE_TASK_STAGE
3587
- else (state.selected_stage
3588
- or ("auto" if state.task_type == "implementation"
3589
- else "(not selected)"))
3583
+ stage = wizard_stage_confirmation_label(
3584
+ task_type=state.task_type,
3585
+ selected_stage=state.selected_stage,
3586
+ whole_task_label=_msg(
3587
+ state.workspace_root, "confirmation", "stage_whole_task"
3588
+ ),
3590
3589
  )
3591
3590
  lines.append(f" stage : {stage}")
3592
3591
  if state.clarification_response_path:
@@ -0,0 +1,54 @@
1
+ """Stage-related intent projection for okstra-run wizard output."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ WHOLE_TASK_STAGE = "__whole_task__"
8
+
9
+
10
+ class WizardStageIntentError(ValueError):
11
+ """Raised when wizard state cannot produce stage-related render args."""
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class WizardStageIntent:
16
+ stage: str
17
+ chain_stages: str
18
+
19
+
20
+ def resolve_wizard_stage_intent(
21
+ *,
22
+ task_type: str,
23
+ selected_stage: str,
24
+ selected_stages: str,
25
+ ) -> WizardStageIntent:
26
+ """Return the stage-related ``okstra render-bundle`` arguments."""
27
+ if task_type == "implementation":
28
+ stage = selected_stage or "auto"
29
+ return WizardStageIntent(stage=stage, chain_stages=selected_stages or stage)
30
+
31
+ if task_type == "final-verification":
32
+ if selected_stage == WHOLE_TASK_STAGE:
33
+ return WizardStageIntent(stage="", chain_stages="")
34
+ if not selected_stage or selected_stage == "auto":
35
+ raise WizardStageIntentError(
36
+ "final-verification requires an explicit stage number"
37
+ )
38
+ return WizardStageIntent(stage=selected_stage, chain_stages="")
39
+
40
+ return WizardStageIntent(stage="", chain_stages="")
41
+
42
+
43
+ def wizard_stage_confirmation_label(
44
+ *,
45
+ task_type: str,
46
+ selected_stage: str,
47
+ whole_task_label: str,
48
+ ) -> str:
49
+ """Return the human-readable stage value for the confirmation block."""
50
+ if selected_stage == WHOLE_TASK_STAGE:
51
+ return whole_task_label
52
+ if selected_stage:
53
+ return selected_stage
54
+ return "auto" if task_type == "implementation" else "(not selected)"
@@ -14,6 +14,7 @@ _HERE = Path(__file__).resolve().parent.parent
14
14
  if str(_HERE) not in sys.path:
15
15
  sys.path.insert(0, str(_HERE))
16
16
 
17
+ from okstra_ctl.final_report_paths import final_report_markdown_path # noqa: E402
17
18
  from okstra_ctl.render_final_report import ( # noqa: E402
18
19
  FinalReportRenderError,
19
20
  render_to_file,
@@ -209,11 +210,7 @@ def populate_and_render(
209
210
  changes = populate_data_token_cells(data_path, team_state)
210
211
 
211
212
  if output_path is None:
212
- name = data_path.name
213
- if name.endswith(".data.json"):
214
- output_path = data_path.with_name(name[: -len(".data.json")] + ".md")
215
- else:
216
- output_path = data_path.with_suffix(".md")
213
+ output_path = final_report_markdown_path(data_path)
217
214
 
218
215
  try:
219
216
  bytes_written = render_to_file(data_path, output_path)
@@ -38,6 +38,7 @@ from okstra_ctl.report_views import ( # noqa: E402
38
38
  plan_approval_context,
39
39
  source_digest,
40
40
  )
41
+ from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
41
42
 
42
43
 
43
44
  _EXTERNAL_URL_RE = re.compile(
@@ -83,7 +84,7 @@ def validate(report_path: Path) -> list[str]:
83
84
  return [f"final-report not found: {report_path}"]
84
85
 
85
86
  md = report_path.read_text(encoding="utf-8")
86
- html_path = report_path.with_name(report_path.stem + ".html")
87
+ html_path = html_view_path(report_path)
87
88
  # §1 헤딩이 있는데 파싱이 실패하면 md_ids 가 빈 []이 되어 "clarification 없음
88
89
  # → skip" 으로 흘러 HTML form parity 게이트가 조용히 열린다. fail-closed.
89
90
  if section_1_present_but_unparsed(md):
@@ -44,6 +44,7 @@ from okstra_ctl.md_table import ( # noqa: E402
44
44
  is_separator_row as _is_markdown_separator,
45
45
  split_pipe_row as _split_pipe_row,
46
46
  )
47
+ from okstra_ctl.final_report_paths import final_report_data_path as _data_path_for # noqa: E402
47
48
 
48
49
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
49
50
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
@@ -1414,17 +1415,6 @@ def _validate_verdict_card_consistency(content: str, failures: list[str]) -> Non
1414
1415
  )
1415
1416
 
1416
1417
 
1417
- def _data_path_for(report_path: Path) -> Path:
1418
- """Derive the final-report data.json sibling from the markdown path.
1419
- ``foo.md`` → ``foo.data.json``. The data.json is the canonical JSON
1420
- SSOT; the markdown is rendered from it.
1421
- """
1422
- name = report_path.name
1423
- if name.endswith(".md"):
1424
- return report_path.with_name(name[:-3] + ".data.json")
1425
- return report_path.with_suffix(".data.json")
1426
-
1427
-
1428
1418
  def _load_final_report_data(report_path: Path) -> dict:
1429
1419
  """Best-effort parse of the final-report data.json sibling. Returns {} when
1430
1420
  absent or unparseable — those conditions are already surfaced as failures by
@@ -13,9 +13,9 @@ const SKILLS_MANIFEST_REL = "installed-skills.json";
13
13
  const AGENTS_MANIFEST_REL = "installed-agents.json";
14
14
  const USER_HOME = homedir();
15
15
  const CLAUDE_HOME = join(USER_HOME, ".claude");
16
- const AGENT_HOME = join(USER_HOME, ".agent");
16
+ const AGENTS_HOME = join(USER_HOME, ".agents");
17
17
  const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
18
- const AGENT_SKILLS_DIR = join(AGENT_HOME, "skills");
18
+ const AGENTS_SKILLS_DIR = join(AGENTS_HOME, "skills");
19
19
  const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
20
20
 
21
21
  // Source template (relative to runtime root in copy mode, or repo root in link mode).
@@ -80,7 +80,7 @@ Effect (copy mode):
80
80
  ${"$HOME"}/.okstra/prompts <- runtime/prompts (lead contracts + coding-preflight pack)
81
81
  ${"$HOME"}/.okstra/templates/settings.local.json <- runtime/templates/reports/settings.template.json
82
82
  ${"$HOME"}/.claude/skills/<name> <- skills when ${"$HOME"}/.claude exists
83
- ${"$HOME"}/.agent/skills/<name> <- skills (created when needed)
83
+ ${"$HOME"}/.agents/skills/<name> <- skills (created when needed)
84
84
  ${"$HOME"}/.claude/agents/<worker>.md <- worker agents when ${"$HOME"}/.claude exists
85
85
  ${"$HOME"}/.okstra/installed-runtimes.json <- install request/resolution manifest
86
86
  ${"$HOME"}/.okstra/installed-skills.json <- target-aware manifest of installed skills
@@ -93,7 +93,7 @@ Effect (link mode):
93
93
  ${"$HOME"}/.okstra/prompts -> <repo>/prompts
94
94
  ${"$HOME"}/.okstra/templates/settings.local.json -> <repo>/templates/reports/settings.template.json
95
95
  ${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> when ${"$HOME"}/.claude exists
96
- ${"$HOME"}/.agent/skills/<name> -> <repo>/skills/<name> (created when needed)
96
+ ${"$HOME"}/.agents/skills/<name> -> <repo>/skills/<name> (created when needed)
97
97
  ${"$HOME"}/.claude/agents/<worker>.md -> <repo>/agents/workers/<worker>.md when ${"$HOME"}/.claude exists
98
98
  ${"$HOME"}/.okstra/dev-link <- <repo> path stamp
99
99
  ${"$HOME"}/.okstra/version <- installed package version stamp
@@ -401,7 +401,7 @@ async function detectSkillTargets() {
401
401
  if (hasClaude) {
402
402
  targets.push({ provider: "claude", home: CLAUDE_HOME, skillsDir: CLAUDE_SKILLS_DIR });
403
403
  }
404
- targets.push({ provider: "agent", home: AGENT_HOME, skillsDir: AGENT_SKILLS_DIR });
404
+ targets.push({ provider: "agent", home: AGENTS_HOME, skillsDir: AGENTS_SKILLS_DIR });
405
405
  return targets;
406
406
  }
407
407
 
@@ -40,13 +40,13 @@ const FALLBACK_AGENT_NAMES = [
40
40
 
41
41
  const USER_HOME = homedir();
42
42
  const CLAUDE_HOME = join(USER_HOME, ".claude");
43
- const AGENT_HOME = join(USER_HOME, ".agent");
43
+ const AGENTS_HOME = join(USER_HOME, ".agents");
44
44
  const CLAUDE_SKILLS_DIR = join(CLAUDE_HOME, "skills");
45
- const AGENT_SKILLS_DIR = join(AGENT_HOME, "skills");
45
+ const AGENTS_SKILLS_DIR = join(AGENTS_HOME, "skills");
46
46
  const CLAUDE_AGENTS_DIR = join(CLAUDE_HOME, "agents");
47
47
  const FALLBACK_SKILL_ROOTS = [
48
48
  CLAUDE_SKILLS_DIR,
49
- AGENT_SKILLS_DIR,
49
+ AGENTS_SKILLS_DIR,
50
50
  ];
51
51
  const SKILLS_MANIFEST_REL = "installed-skills.json";
52
52
  const AGENTS_MANIFEST_REL = "installed-agents.json";
@@ -60,7 +60,7 @@ Usage:
60
60
  installed-skills.json, installed-agents.json} AND
61
61
  every skill target recorded in
62
62
  installed-skills.json
63
- (~/.claude/skills or ~/.agent/skills).
63
+ (~/.claude/skills or ~/.agents/skills).
64
64
  Claude worker agents are
65
65
  removed from ~/.claude/agents when installed.
66
66
  Missing manifests fall back to hard-coded