okstra 0.102.2 → 0.102.4

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 (167) hide show
  1. package/README.kr.md +5 -5
  2. package/README.md +5 -5
  3. package/docs/kr/architecture/storage-model.md +8 -3
  4. package/docs/kr/architecture.md +12 -13
  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/lib/okstra/interactive.sh +12 -0
  12. package/runtime/bin/okstra-antigravity-exec.sh +1 -2
  13. package/runtime/bin/okstra-codex-exec.sh +1 -2
  14. package/runtime/bin/okstra-render-final-report.py +2 -9
  15. package/runtime/bin/okstra-spawn-followups.py +4 -6
  16. package/runtime/bin/okstra-trace-cleanup.sh +24 -4
  17. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  18. package/runtime/prompts/lead/report-writer.md +1 -1
  19. package/runtime/prompts/profiles/_common-contract.md +9 -11
  20. package/runtime/python/okstra_ctl/codex_dispatch.py +6 -13
  21. package/runtime/python/okstra_ctl/consumers.py +35 -0
  22. package/runtime/python/okstra_ctl/container.py +4 -6
  23. package/runtime/python/okstra_ctl/dispatch_core.py +8 -13
  24. package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
  25. package/runtime/python/okstra_ctl/handoff.py +2 -1
  26. package/runtime/python/okstra_ctl/implementation_outcome.py +271 -0
  27. package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
  28. package/runtime/python/okstra_ctl/path_hints.py +665 -0
  29. package/runtime/python/okstra_ctl/render.py +4 -3
  30. package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
  31. package/runtime/python/okstra_ctl/report_views.py +7 -4
  32. package/runtime/python/okstra_ctl/run.py +3 -10
  33. package/runtime/python/okstra_ctl/run_context.py +4 -2
  34. package/runtime/python/okstra_ctl/stage_targets.py +18 -0
  35. package/runtime/python/okstra_ctl/wizard.py +37 -26
  36. package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
  37. package/runtime/python/okstra_project/state.py +40 -2
  38. package/runtime/python/okstra_token_usage/report.py +2 -5
  39. package/runtime/validators/validate-report-views.py +2 -1
  40. package/runtime/validators/validate-run.py +1 -11
  41. package/src/commands/lifecycle/install.mjs +5 -5
  42. package/src/commands/lifecycle/uninstall.mjs +4 -4
  43. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
  44. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
  45. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
  46. package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
  47. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
  48. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
  49. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
  50. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
  51. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
  52. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
  53. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
  54. package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
  55. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
  56. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
  57. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
  58. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
  59. package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
  60. package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
  61. package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
  62. package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
  63. package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
  64. package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
  65. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
  66. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
  67. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
  68. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
  69. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
  70. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
  71. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
  72. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
  73. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
  74. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
  75. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
  76. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
  77. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
  78. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
  79. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
  80. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
  81. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
  82. package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
  83. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
  84. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
  85. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
  86. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
  87. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
  88. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
  89. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
  90. package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
  91. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
  92. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
  93. package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
  94. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
  95. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
  96. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
  97. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
  98. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
  99. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
  100. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
  101. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
  102. package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
  103. package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
  104. package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
  105. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
  106. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
  107. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
  108. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
  109. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
  110. package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
  111. package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
  112. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
  113. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
  114. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
  115. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
  116. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
  117. package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
  118. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
  119. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
  120. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
  121. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
  122. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
  123. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
  124. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
  125. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
  126. package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
  127. package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
  128. package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
  129. package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
  130. package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
  131. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
  132. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
  133. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
  134. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
  135. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
  136. package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
  137. package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
  138. package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
  139. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
  140. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
  141. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
  142. package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
  143. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
  144. package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
  145. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
  146. package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
  147. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
  148. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
  149. package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
  150. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
  151. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
  152. package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
  153. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
  154. package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
  155. package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
  156. package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
  157. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
  158. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
  159. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
  160. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
  161. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
  162. package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
  163. package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
  164. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
  165. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
  166. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
  167. package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
@@ -31,6 +31,7 @@ from okstra_project.dirs import OKSTRA_DIR_NAME, project_json_path
31
31
  from . import fix_cycles
32
32
  from .paths import okstra_home
33
33
  from .lead_runtime import lead_runtime_info
34
+ from .path_hints import compact_active_run_context, hydrate_run_context
34
35
  from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
35
36
 
36
37
 
@@ -56,8 +57,8 @@ def _load_ctx(ctx_arg: str) -> dict:
56
57
  """
57
58
  s = ctx_arg.lstrip()
58
59
  if s.startswith("{"):
59
- return json.loads(ctx_arg)
60
- return json.loads(Path(ctx_arg).read_text(encoding="utf-8"))
60
+ return hydrate_run_context(json.loads(ctx_arg))
61
+ return hydrate_run_context(json.loads(Path(ctx_arg).read_text(encoding="utf-8")))
61
62
 
62
63
 
63
64
  def _write_text(path: Path, text: str) -> None:
@@ -434,7 +435,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
434
435
  "sourceArtifacts": _active_source_artifacts(ctx),
435
436
  "lazyReadPlan": _active_lazy_read_plan(),
436
437
  }
437
- _write_json(Path(active_context_path), payload)
438
+ _write_json(Path(active_context_path), compact_active_run_context(ctx, payload))
438
439
 
439
440
 
440
441
  def render_team_state(team_state_path: str, ctx: dict) -> None:
@@ -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)
@@ -25,6 +25,7 @@ from typing import Iterator, Optional
25
25
 
26
26
  from okstra_project.dirs import okstra_home
27
27
 
28
+ from .path_hints import compact_run_context, hydrate_run_context
28
29
  from .paths import compute_run_paths, task_runs_dir
29
30
 
30
31
 
@@ -188,7 +189,7 @@ def compute_and_write_run_context(
188
189
  str(ctx_path.relative_to(project_root.resolve()))
189
190
  if ctx_path.resolve().is_relative_to(project_root.resolve())
190
191
  else str(ctx_path))
191
- _atomic_write_json(ctx_path, ctx)
192
+ _atomic_write_json(ctx_path, compact_run_context(ctx))
192
193
  return ctx
193
194
 
194
195
 
@@ -225,7 +226,8 @@ def read_run_context(run_manifests_dir: Path, task_type_segment: str,
225
226
  path = Path(run_manifests_dir) / _run_context_filename(task_type_segment, seq)
226
227
  if not path.is_file():
227
228
  return None
228
- return json.loads(path.read_text(encoding="utf-8"))
229
+ payload = json.loads(path.read_text(encoding="utf-8"))
230
+ return hydrate_run_context(payload)
229
231
 
230
232
 
231
233
  def read_run_inputs(run_manifests_dir: Path, task_type_segment: str,
@@ -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:
@@ -1046,6 +1053,18 @@ def _opt(value: str, label: str = "", description: str = "") -> Option:
1046
1053
 
1047
1054
  # --- builders ---
1048
1055
 
1056
+ def _contract_outcome_suffix(entry: dict) -> str:
1057
+ outcome = entry.get("phaseOutcome")
1058
+ implementation = outcome.get("implementation") if isinstance(outcome, dict) else {}
1059
+ if not isinstance(implementation, dict):
1060
+ return ""
1061
+ if implementation.get("state") != "completed":
1062
+ return ""
1063
+ if entry.get("currentStatus") == "contract-violated":
1064
+ return " · implementation complete; contract warnings"
1065
+ return ""
1066
+
1067
+
1049
1068
  def _build_task_pick(state: WizardState) -> Prompt:
1050
1069
  t = _p(state.workspace_root, "task_pick")
1051
1070
  project_root = Path(state.project_root)
@@ -1063,7 +1082,7 @@ def _build_task_pick(state: WizardState) -> Prompt:
1063
1082
  phase = entry.get("currentPhase") or ttype
1064
1083
  nxt = entry.get("nextRecommendedPhase") or ""
1065
1084
  suffix = latest_suffix if key == latest_key else ""
1066
- label = f"{key} · {phase} · next: {nxt}{suffix}"
1085
+ label = f"{key} · {phase} · next: {nxt}{_contract_outcome_suffix(entry)}{suffix}"
1067
1086
  options.append(_opt(value=key, label=label))
1068
1087
  for value, label in _static_options(t):
1069
1088
  options.append(_opt(value=value, label=label))
@@ -1652,7 +1671,6 @@ _REUSE_LAST_TOKEN = "__reuse_last__"
1652
1671
  _SIBLINGS_TOKEN = "__siblings__"
1653
1672
  _LATEST_REPORT_TOKEN = "__latest_report__"
1654
1673
  _PROJECT_DEFAULT_TOKEN = "__project_default__"
1655
- WHOLE_TASK_STAGE = "__whole_task__"
1656
1674
  ALL_STAGES = "__all_stages__"
1657
1675
 
1658
1676
 
@@ -3488,21 +3506,14 @@ def render_args(state: WizardState) -> dict[str, str]:
3488
3506
  if state.reuse_worktree or state.task_type == "final-verification"
3489
3507
  else state.base_ref
3490
3508
  )
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 = ""
3509
+ try:
3510
+ stage_intent = resolve_wizard_stage_intent(
3511
+ task_type=state.task_type,
3512
+ selected_stage=state.selected_stage,
3513
+ selected_stages=state.selected_stages,
3514
+ )
3515
+ except WizardStageIntentError as exc:
3516
+ raise WizardError(str(exc)) from exc
3506
3517
  pr_template = (
3507
3518
  state.pr_template_path
3508
3519
  if state.task_type == "release-handoff"
@@ -3518,9 +3529,9 @@ def render_args(state: WizardState) -> dict[str, str]:
3518
3529
  "executor": state.executor,
3519
3530
  "critic": state.critic,
3520
3531
  "approved-plan": state.approved_plan_path,
3521
- "stage": stage,
3532
+ "stage": stage_intent.stage,
3522
3533
  "stages": state.handoff_stages,
3523
- "chain-stages": chain_stages,
3534
+ "chain-stages": stage_intent.chain_stages,
3524
3535
  "base-ref": base_ref,
3525
3536
  "workers": workers,
3526
3537
  "directive": state.directive,
@@ -3581,12 +3592,12 @@ def confirmation_block(state: WizardState) -> str:
3581
3592
  lines.append(f" critic : {state.critic or '(off)'}")
3582
3593
  if state.task_type in _STAGE_SCOPED_TASK_TYPES:
3583
3594
  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)"))
3595
+ stage = wizard_stage_confirmation_label(
3596
+ task_type=state.task_type,
3597
+ selected_stage=state.selected_stage,
3598
+ whole_task_label=_msg(
3599
+ state.workspace_root, "confirmation", "stage_whole_task"
3600
+ ),
3590
3601
  )
3591
3602
  lines.append(f" stage : {stage}")
3592
3603
  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)"
@@ -1,10 +1,13 @@
1
- """Read-only state accessors over okstra's on-disk authority files.
1
+ """Read-side state accessors over okstra's on-disk authority files.
2
2
 
3
3
  Skills, okstra.sh, and okstra-ctl all read the same files through this module
4
4
  so identity / discovery values are derived from disk on every call instead of
5
5
  being passed via process environment. This keeps concurrent claude-code skill
6
6
  invocations isolated — each call reads the authoritative state at the moment
7
7
  it runs and never sees stale snapshots inherited from a parent process.
8
+ Some accessors also run best-effort reconciliation before returning derived
9
+ phase pointers, so stale catalog entries can be healed from authoritative
10
+ task artifacts.
8
11
 
9
12
  권위 파일 매핑 (okstra root = <PROJECT_ROOT>/<OKSTRA_DIR_NAME>):
10
13
  - <okstra root>/project.json
@@ -78,6 +81,38 @@ def read_task_manifest(task_root: Path) -> Optional[dict]:
78
81
  return _load_json(Path(task_root) / "task-manifest.json")
79
82
 
80
83
 
84
+ def _reconcile_task_root_best_effort(project_root: Path, task_root: Path) -> None:
85
+ try:
86
+ from okstra_ctl.implementation_outcome import reconcile_implementation_outcome
87
+
88
+ reconcile_implementation_outcome(project_root, task_root)
89
+ except Exception:
90
+ return
91
+
92
+
93
+ def _entry_with_manifest_state(entry: dict, task_root: Path) -> dict:
94
+ out = {**entry}
95
+ manifest = read_task_manifest(task_root) or {}
96
+ workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
97
+ for source, target in (
98
+ ("currentPhase", "currentPhase"),
99
+ ("currentPhaseState", "currentPhaseState"),
100
+ ("lastCompletedPhase", "lastCompletedPhase"),
101
+ ("nextRecommendedPhase", "nextRecommendedPhase"),
102
+ ):
103
+ value = workflow.get(source)
104
+ if isinstance(value, str):
105
+ out[target] = value
106
+ for field in ("currentStatus", "latestRunStatus", "latestReportPath"):
107
+ value = manifest.get(field)
108
+ if isinstance(value, str):
109
+ out[field] = value
110
+ phase_outcome = manifest.get("phaseOutcome")
111
+ if isinstance(phase_outcome, dict):
112
+ out["phaseOutcome"] = phase_outcome
113
+ return out
114
+
115
+
81
116
  def parse_task_key(task_key: str) -> tuple[str, str, str]:
82
117
  """`project-id:task-group:task-id` 를 3-tuple 로 분해.
83
118
 
@@ -111,10 +146,12 @@ def find_task_root(project_root: Path, task_key: str) -> Optional[Path]:
111
146
  if isinstance(rel, str) and rel:
112
147
  abs_path = project_root / rel if not Path(rel).is_absolute() else Path(rel)
113
148
  if abs_path.is_dir():
149
+ _reconcile_task_root_best_effort(project_root, abs_path)
114
150
  return abs_path
115
151
 
116
152
  slug_path = project_root / TASKS_RELATIVE / slugify(task_group) / slugify(task_id)
117
153
  if slug_path.is_dir():
154
+ _reconcile_task_root_best_effort(project_root, slug_path)
118
155
  return slug_path
119
156
  return None
120
157
 
@@ -180,7 +217,8 @@ def list_project_tasks(
180
217
  root = find_task_root(project_root, entry_key)
181
218
  if root is None:
182
219
  continue
183
- out.append({**entry, "_resolvedTaskRoot": str(root)})
220
+ reconciled_entry = _entry_with_manifest_state(entry, root)
221
+ out.append({**reconciled_entry, "_resolvedTaskRoot": str(root)})
184
222
  if limit is not None and limit >= 0:
185
223
  out = out[:limit]
186
224
  return out
@@ -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