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
@@ -49,7 +49,7 @@ This document is the operating contract and phase index. Detailed procedures liv
49
49
  | 5.5 Convergence | Cross-verify findings across workers | `convergence` |
50
50
  | 5.6 Critic pass | (opt-in) reused-worker critic pass: coverage gaps (discovery/error-analysis/impl-planning) or acceptance devil's-advocate (final-verification). The critic dispatch fires concurrently with the first 5.5 reverify round (its input is fixed at Round 0); gap/blocker verification (one round) completes here | `convergence` "Coverage critic pass" / "Acceptance critic pass" |
51
51
  | 6. Synthesis | Dispatch Report writer worker, review draft. **For `implementation-planning`: then run the Phase 6 plan-body verification sub-step (see Phase 6 section below).** | `report-writer` + `convergence` (sub-step) |
52
- | 7. Persist | Run token-usage collector, update manifests, ask about residual tmux panes, then ask whether to dismiss the worker teammates (reconcile stale members + `SendMessage` shutdown_request only on approval) | `report-writer` + `_common-contract.md` "Phase wrap-up" / "Run-end teammate teardown" |
52
+ | 7. Persist | Run token-usage collector, update manifests, ask once about residual tmux panes plus worker teammates, then close panes and dismiss teammates together on approval (reconcile stale members + `SendMessage` shutdown_request) | `report-writer` + `_common-contract.md` "Phase wrap-up" / "Run-end teammate teardown" |
53
53
 
54
54
  ## Core operating contract
55
55
 
@@ -103,7 +103,7 @@ Required checkpoints:
103
103
  - `PROGRESS: phase-batch-cleanup panes=<n> teammates=<m>` — 이전 배치 pane·완료 teammate 를 정리한 직후, 각 배치 경계에서(① 첫 `phase-5.5-convergence` round 직전 ② `phase-6-synthesis` report-writer dispatch 직전). 카운트만 노출하고 `%NNN`/lead-pane.id/raw Agent name 은 절대 노출하지 않는다. 첫 배치(분석 워커 dispatch) 직전엔 치울 게 없어 no-op 이며 마커도 생략.
104
104
  - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6.
105
105
  - `PROGRESS: phase-7-persist updating manifests` — at the start of Phase 7.
106
- - `PROGRESS: phase-7-teardown dismissing teammates` — after token-usage collection and after the pane-disposition prompt, only when the user approved worker teammate cleanup, immediately before reconciling stale members + dismissing them via `SendMessage` shutdown_request (Teams mode only; see `_common-contract.md` "Run-end teammate teardown"). Skipped when split-pane is off (no roster) or when the user keeps the teammates.
106
+ - `PROGRESS: phase-7-teardown dismissing teammates` — after token-usage collection and after the pane-disposition prompt, only when the user approved combined pane/team cleanup, immediately before reconciling stale members + dismissing them via `SendMessage` shutdown_request (Teams mode only; see `_common-contract.md` "Run-end teammate teardown"). Skipped when split-pane is off (no roster) or when the user keeps the panes/teammates.
107
107
  - `PROGRESS: complete final-report=<relative-path>` — final summary line, after all persistence.
108
108
 
109
109
  These lines are the only structured signal the user has during a long run. Do NOT replace them with prose ("Now I'm starting Phase 2..."), do NOT skip a checkpoint because "the previous message already said that", and do NOT batch multiple checkpoints into one. Each line stands alone so the user (or any operator scraping stdout) can timestamp it externally.
@@ -130,7 +130,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
130
130
 
131
131
  The status file is written after step 3 completes.
132
132
 
133
- **Run-end teammate teardown follows this whole sequence.** Token-usage collection (step 1) reads the worker session jsonls, so the lead MUST NOT disband the team until every step above is done. Only then — after the pane-disposition prompt and only when the user approves teammate cleanup — does the lead dismiss the worker teammates via `SendMessage` shutdown_request per `_common-contract.md` "Run-end teammate teardown" (split-pane runs only; silent-skip when split-pane is off; if the user keeps the teammates, leave them intact and surface the manual Teams/FleetView cleanup path).
133
+ **Run-end teammate teardown follows this whole sequence.** Token-usage collection (step 1) reads the worker session jsonls, so the lead MUST NOT disband the team until every step above is done. Only then — after the combined pane/team disposition prompt and only when the user approves cleanup — does the lead dismiss the worker teammates via `SendMessage` shutdown_request per `_common-contract.md` "Run-end teammate teardown" (split-pane runs only; silent-skip when split-pane is off; if the user keeps panes/teammates, leave them intact and surface the manual Teams/FleetView cleanup path).
134
134
 
135
135
  ## Final Report Structure
136
136
 
@@ -44,25 +44,23 @@ profile document.
44
44
  - Phase wrap-up — okstra pane disposition (shared, runs AFTER Phase 7 persistence/token collection and BEFORE teammate teardown):
45
45
  - At run end the only residual okstra panes are the LAST phase's (e.g. the `report-writer-worker` agent pane and any codex/antigravity trace pane). `okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` returns one tab-separated `<pane_id>\t<pane_title>` line per residual okstra pane (worker-agent + trace) for this run.
46
46
  - After the final-report file has been written and the routing recommendation has been issued, the lead MUST ALWAYS run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` exactly once — NEVER gating this call on its own reading of `lead-pane.id`. The output lists every residual okstra pane (worker-agent + trace) for this run, never the lead's own pane; outside tmux it is a safe no-op (empty output).
47
- - If the list is empty, skip the question — there is nothing to ask about (the phase-start resets above usually already cleared prior phases).
48
- - Otherwise the lead MUST present the user with a strict binary choice **before** declaring the phase complete. Use one prompt of this shape (Korean preferred, English acceptable if the rest of the run is in English):
49
- > 현재 phase 종료 시점입니다. 다음 okstra pane 열려 있습니다 — 닫을까요?
47
+ - If the list is empty and no matching on-disk team config exists (see Run-end teammate teardown below), skip the question — there is nothing to ask about (the phase-start resets above usually already cleared prior phases).
48
+ - Otherwise the lead MUST present the user with a strict binary choice **before** declaring the phase complete. This single choice controls BOTH residual pane cleanup and worker teammate dismissal; do not ask a second teammate-cleanup question. Use one prompt of this shape (Korean preferred, English acceptable if the rest of the run is in English):
49
+ > 현재 phase 종료 시점입니다. 다음 okstra pane worker teammate 가 남아 있습니다 — 닫고 정리할까요?
50
50
  > <인용된 `--list` 출력>
51
- > (예) 모두 닫기 / (아니오) 그대로 두기
52
- - On `예` / `y` / `close` → run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` and report the kill count back in one sentence.
53
- - On `아니오` / `n` / `keep` → leave the panes intact; remind the user that they will be cleaned up automatically when Claude `/exit` fires the `SessionEnd` hook (`--reap`).
54
- - The question MUST be a clean yes/no — do NOT offer "close some / keep some" partial answers, do NOT propose alternatives like "close only codex panes". The whole-set decision keeps the wrap-up predictable.
51
+ > (예) 모두 닫고 팀원 정리 / (아니오) 그대로 두기
52
+ - On `예` / `y` / `close` → run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`, then run the Run-end teammate teardown sequence below without asking a second question. Report the pane kill count and teammate cleanup outcome back in one sentence.
53
+ - On `아니오` / `n` / `keep` → leave panes and teammates intact; remind the user that tagged trace panes will be cleaned up automatically when Claude `/exit` fires the `SessionEnd` hook (`--reap`), while FleetView/Teams teammates remain visible until session end or manual Teams UI cleanup.
54
+ - The question MUST be a clean yes/no — do NOT offer "close some / keep some" partial answers, do NOT propose alternatives like "close only codex panes". The whole-set decision keeps the wrap-up predictable: lead pane excluded, every okstra worker/tail pane and worker teammate cleaned together on approval.
55
55
  - This step is mandatory for every phase (`requirements-discovery`, `error-analysis`, `implementation-planning`, `implementation`, `final-verification`, `release-handoff`). Whether there is anything to ask about is decided by the `--list` output, NOT by the lead's reading of `lead-pane.id`: an empty list → skip the question (above); the lead MUST NOT fabricate a synthetic pane list.
56
56
  - Run-end teammate teardown (shared — MUST run AFTER the pane disposition question and BEFORE `PROGRESS: complete`):
57
57
  - CC v2.1.178 removed `TeamCreate` / `TeamDelete`: this run rode the session's **implicit team** (Phase 3 recorded `teamCreate.status: "implicit"` plus the `teamName` audit label). There is no tool to delete a team — the implicit team ends when the session ends. What teardown CAN do is gracefully dismiss the worker teammates the lead spawned, so they stop showing as live members in the FleetView roster.
58
58
  - **Whether to run this step is decided by on-disk fact.** The harness names the team directory `session-<leadSessionId-prefix>` (the `teamName` label never becomes a directory name). Resolve existence from disk: find the `~/.claude/teams/session-*/config.json` whose `leadSessionId` equals team-state `lead.sessionId`.
59
59
  - **Matching on-disk config found, with members beyond the lead** → teammates exist; run the dismissal sequence below.
60
60
  - **No matching on-disk config, or split-pane was off** → nothing to dismiss → silent-skip.
61
- - After the pane disposition step above, the lead MUST ask the user whether to dismiss the worker teammates. This is a strict binary choice:
62
- > worker teammate (`<teamName>`) 들을 정리할까요?
63
- > (예) 팀원 정리 / (아니오) 그대로 두기
61
+ - The pane disposition prompt above is the only user prompt for this cleanup decision. Do NOT ask a second teammate-only question.
64
62
  - On `아니오` / `n` / `keep` → dismiss nothing. Tell the user the teammates remain visible in FleetView/Teams until the session ends, and can be removed earlier from the Teams UI.
65
- - On `예` / `y` / `cleanup` → emit `PROGRESS: phase-7-teardown dismissing teammates`, then run the sequence below. Token-usage collection MUST already be complete — it reads `~/.claude/projects/` jsonls, which dismissal never touches, but the read MUST precede teardown.
63
+ - On `예` / `y` / `close` → emit `PROGRESS: phase-7-teardown dismissing teammates`, then run the sequence below. Token-usage collection MUST already be complete — it reads `~/.claude/projects/` jsonls, which dismissal never touches, but the read MUST precede teardown.
66
64
  1. Run `$HOME/.okstra/bin/okstra-team-reconcile.sh "<resolved-config-path>"` — the on-disk `~/.claude/teams/session-<…>/config.json` resolved in the existence check above — exactly once. It flips dead-pane stale-active members to inactive, and no-ops when tmux is unavailable or nothing is stale. Do NOT loop it.
67
65
  2. Send each confirmed-complete member a structured `SendMessage(to: <name>, message: { type: "shutdown_request" })` — the `message` MUST be the object literal shown, NEVER a JSON string stuffed into a text field. NEVER target the lead's own session, and never use `TaskStop` (teammates are not background tasks — `TaskStop` 404s on a member address).
68
66
  - Claude Code exposes no tool to delete the team or surgically remove a roster entry, so a dismissed teammate may still show as an idle pill until the session ends. Do not pretend the team was deleted. If the user wants the roster gone entirely, surface the manual action: `Teams/FleetView 에서 team <teamName> 삭제`, and if tmux panes were kept earlier also show `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`.
@@ -18,7 +18,12 @@ 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
26
+ from .path_hints import hydrate_active_run_context
22
27
  from .wrapper_status import status_path_for_prompt
23
28
  from .paths import task_dir
24
29
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
@@ -1398,7 +1403,7 @@ def _load_json_object(path: Path, label: str) -> dict[str, Any]:
1398
1403
  def _load_optional_json_object(path: Path | None) -> dict[str, Any]:
1399
1404
  if path is None or not path.is_file():
1400
1405
  return {}
1401
- return _load_json_object(path, "active-run-context")
1406
+ return hydrate_active_run_context(_load_json_object(path, "active-run-context"))
1402
1407
 
1403
1408
 
1404
1409
  def _resolve_required_path(
@@ -1450,18 +1455,6 @@ def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
1450
1455
  return "worker dispatch failed"
1451
1456
 
1452
1457
 
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
1458
  def _replace_report_writer_model_line(
1466
1459
  prompt_path: Path,
1467
1460
  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,7 +11,12 @@ 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
19
+ from .path_hints import hydrate_active_run_context
15
20
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
16
21
  from .wrapper_status import read_wrapper_status, status_path_for_prompt
17
22
 
@@ -823,18 +828,6 @@ def _completion_paths(worker_id: str, result_path: Path, manifest: Mapping[str,
823
828
  return (data_json, _final_report_markdown_path(data_json), result_path)
824
829
 
825
830
 
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
831
  def _run_dir(plan: DispatchPlan) -> Path:
839
832
  value = _string_value(plan.manifest.get("runDirectoryPath"))
840
833
  return _resolve_project_path(plan.project_root, value) if value else plan.team_state_path.parent.parent
@@ -876,7 +869,9 @@ def _load_optional_json(project_root: Path, value: Any) -> dict[str, Any]:
876
869
  if not isinstance(value, str) or not value.strip():
877
870
  return {}
878
871
  path = _resolve_project_path(project_root, value)
879
- return _load_json_object(path, "active-run-context") if path.is_file() else {}
872
+ if not path.is_file():
873
+ return {}
874
+ return hydrate_active_run_context(_load_json_object(path, "active-run-context"))
880
875
 
881
876
 
882
877
  def _resolve_required_path(project_root: Path, manifest: Mapping[str, Any], key: str) -> Path:
@@ -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
 
@@ -0,0 +1,271 @@
1
+ """Artifact-derived implementation phase outcome reconciliation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .consumers import read_stage_consumer_state
10
+ from .workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ImplementationOutcome:
15
+ completed: bool
16
+ stages: list[int] = field(default_factory=list)
17
+ head_commit: str = ""
18
+ report_path: str = ""
19
+ next_recommended_phase: str = ""
20
+ reason: str = ""
21
+
22
+
23
+ def derive_implementation_outcome(task_root: Path) -> ImplementationOutcome:
24
+ task_root = Path(task_root)
25
+ manifest = _load_json(task_root / "task-manifest.json")
26
+ workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
27
+ if workflow.get("currentPhase") != "implementation":
28
+ return ImplementationOutcome(completed=False, reason="current phase is not implementation")
29
+
30
+ plan_run_root = task_root / "runs" / "implementation-planning"
31
+ if not plan_run_root.is_dir():
32
+ return ImplementationOutcome(completed=False, reason="implementation-planning run root missing")
33
+
34
+ stage_map = _load_stage_map(task_root, manifest)
35
+ if not stage_map:
36
+ return ImplementationOutcome(completed=False, reason="stage map unavailable")
37
+
38
+ consumers = read_stage_consumer_state(plan_run_root, recover_from_carry=True)
39
+ required_stages = {stage["stage_number"] for stage in stage_map}
40
+ if not required_stages or not required_stages.issubset(consumers.done_stages):
41
+ return ImplementationOutcome(completed=False, reason="not all stages are done")
42
+
43
+ carries = [_load_carry(task_root, stage) for stage in sorted(required_stages)]
44
+ if any(carry is None for carry in carries):
45
+ return ImplementationOutcome(completed=False, reason="carry sidecar missing")
46
+ if any(not _carry_passed(carry) for carry in carries if carry is not None):
47
+ return ImplementationOutcome(completed=False, reason="carry sidecar is not pass-grade")
48
+
49
+ latest_stage = max(required_stages)
50
+ latest_done = consumers.done_by_stage.get(latest_stage) or {}
51
+ latest_carry = carries[-1] or {}
52
+ return ImplementationOutcome(
53
+ completed=True,
54
+ stages=sorted(required_stages),
55
+ head_commit=str(latest_done.get("head_commit") or "") or _carry_head_commit(latest_carry),
56
+ report_path=_latest_implementation_report(task_root),
57
+ next_recommended_phase=DEFAULT_NEXT_PHASE["implementation"],
58
+ reason="all implementation stages have pass-grade carry and done rows",
59
+ )
60
+
61
+
62
+ def reconcile_implementation_outcome(project_root: Path, task_root: Path) -> bool:
63
+ project_root = Path(project_root)
64
+ task_root = Path(task_root)
65
+ outcome = derive_implementation_outcome(task_root)
66
+ if not outcome.completed:
67
+ return False
68
+
69
+ manifest_path = task_root / "task-manifest.json"
70
+ manifest = _load_json(manifest_path)
71
+ workflow = manifest.get("workflow")
72
+ if not isinstance(workflow, dict):
73
+ workflow = {}
74
+ manifest["workflow"] = workflow
75
+
76
+ changed = _promote_workflow(workflow, outcome)
77
+ phase_outcome = manifest.get("phaseOutcome")
78
+ if not isinstance(phase_outcome, dict):
79
+ phase_outcome = {}
80
+ manifest["phaseOutcome"] = phase_outcome
81
+
82
+ contract = manifest.get("contractValidation")
83
+ contract_status = contract.get("status", "") if isinstance(contract, dict) else ""
84
+ implementation = {
85
+ "state": "completed",
86
+ "source": "implementation-carry",
87
+ "stages": outcome.stages,
88
+ "headCommit": outcome.head_commit,
89
+ "reportPath": outcome.report_path,
90
+ "contractValidationStatus": contract_status,
91
+ "reason": outcome.reason,
92
+ }
93
+ if phase_outcome.get("implementation") != implementation:
94
+ phase_outcome["implementation"] = implementation
95
+ changed = True
96
+
97
+ if changed:
98
+ _write_json(manifest_path, manifest)
99
+ return changed or _update_task_catalog(project_root, manifest)
100
+
101
+
102
+ def _load_json(path: Path) -> dict[str, Any]:
103
+ if not path.is_file():
104
+ return {}
105
+ try:
106
+ data = json.loads(path.read_text(encoding="utf-8"))
107
+ except json.JSONDecodeError:
108
+ return {}
109
+ return data if isinstance(data, dict) else {}
110
+
111
+
112
+ def _load_stage_map(task_root: Path, manifest: dict[str, Any]) -> list[dict[str, Any]]:
113
+ plan_path = _source_plan_path(task_root, manifest)
114
+ if not plan_path.is_file():
115
+ return []
116
+ from .run import PrepareError, _load_parsed_stage_map, _stage_map_reject_detail
117
+
118
+ try:
119
+ stages, errs = _load_parsed_stage_map(plan_path.read_text(encoding="utf-8"))
120
+ except PrepareError:
121
+ return []
122
+ if _stage_map_reject_detail(stages, errs) is not None:
123
+ return []
124
+ return [
125
+ {
126
+ "stage_number": stage.stage_number,
127
+ "title": stage.title,
128
+ "depends_on": list(stage.depends_on),
129
+ "step_count": stage.step_count,
130
+ }
131
+ for stage in stages
132
+ ]
133
+
134
+
135
+ def _source_plan_path(task_root: Path, manifest: dict[str, Any]) -> Path:
136
+ carry_dir = task_root / "runs" / "implementation" / "carry"
137
+ for carry_path in sorted(carry_dir.glob("stage-*.json")):
138
+ value = _load_json(carry_path).get("sourcePlanPath")
139
+ if isinstance(value, str) and value:
140
+ return _resolve_relative(task_root, value)
141
+
142
+ latest = manifest.get("latestReportPath")
143
+ if isinstance(latest, str) and "implementation-planning" in latest:
144
+ return _resolve_relative(task_root, latest)
145
+
146
+ reports = task_root / "runs" / "implementation-planning" / "reports"
147
+ candidates = sorted(reports.glob("final-report-implementation-planning-*.md"))
148
+ return candidates[-1] if candidates else Path()
149
+
150
+
151
+ def _resolve_relative(task_root: Path, value: str) -> Path:
152
+ path = Path(value)
153
+ if path.is_absolute():
154
+ return path
155
+ project_root = _project_root_from_task_root(task_root)
156
+ project_relative = project_root / path
157
+ if project_relative.exists():
158
+ return project_relative
159
+ task_relative = task_root / path
160
+ if task_relative.exists():
161
+ return task_relative
162
+ return project_relative
163
+
164
+
165
+ def _project_root_from_task_root(task_root: Path) -> Path:
166
+ parts = task_root.resolve().parts
167
+ if ".okstra" not in parts:
168
+ return task_root.resolve()
169
+ return Path(*parts[: parts.index(".okstra")])
170
+
171
+
172
+ def _load_carry(task_root: Path, stage: int) -> dict[str, Any] | None:
173
+ data = _load_json(task_root / "runs" / "implementation" / "carry" / f"stage-{stage}.json")
174
+ return data or None
175
+
176
+
177
+ def _carry_passed(carry: dict[str, Any]) -> bool:
178
+ status = str(carry.get("status") or "").lower()
179
+ if status in {"fail", "failed", "blocked", "error", "aborted"}:
180
+ return False
181
+ conformance = carry.get("conformance")
182
+ if isinstance(conformance, dict):
183
+ overall = str(conformance.get("overall") or "").upper()
184
+ if overall and overall != "PASS":
185
+ return False
186
+ unverified = carry.get("unverified")
187
+ return not (isinstance(unverified, list) and unverified)
188
+
189
+
190
+ def _carry_head_commit(carry: dict[str, Any]) -> str:
191
+ stage_range = carry.get("stageCommitRange")
192
+ if isinstance(stage_range, dict) and stage_range.get("head"):
193
+ return str(stage_range["head"])
194
+ for key in ("head_sha", "head_commit", "head"):
195
+ value = carry.get(key)
196
+ if value:
197
+ return str(value)
198
+ commits = carry.get("commits")
199
+ if isinstance(commits, list) and commits:
200
+ last = commits[-1]
201
+ if isinstance(last, dict) and last.get("sha"):
202
+ return str(last["sha"])
203
+ return ""
204
+
205
+
206
+ def _latest_implementation_report(task_root: Path) -> str:
207
+ reports = sorted(
208
+ (task_root / "runs" / "implementation").glob(
209
+ "stage-*/reports/final-report-implementation-*.md"
210
+ )
211
+ )
212
+ if not reports:
213
+ return ""
214
+ project_root = _project_root_from_task_root(task_root)
215
+ try:
216
+ return str(reports[-1].relative_to(project_root))
217
+ except ValueError:
218
+ return str(reports[-1])
219
+
220
+
221
+ def _promote_workflow(workflow: dict[str, Any], outcome: ImplementationOutcome) -> bool:
222
+ before = json.dumps(workflow, sort_keys=True, ensure_ascii=False)
223
+ phase_states = workflow.get("phaseStates")
224
+ if not isinstance(phase_states, dict):
225
+ phase_states = {}
226
+ workflow["phaseStates"] = phase_states
227
+ for phase in PHASE_SEQUENCE:
228
+ phase_states.setdefault(phase, "not-started")
229
+ workflow["currentPhase"] = "implementation"
230
+ workflow["currentPhaseState"] = "completed"
231
+ phase_states["implementation"] = "completed"
232
+ workflow["lastCompletedPhase"] = "implementation"
233
+ workflow["nextRecommendedPhase"] = outcome.next_recommended_phase
234
+ after = json.dumps(workflow, sort_keys=True, ensure_ascii=False)
235
+ return before != after
236
+
237
+
238
+ def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
239
+ catalog_path = project_root / ".okstra" / "discovery" / "task-catalog.json"
240
+ catalog = _load_json(catalog_path)
241
+ tasks = catalog.get("tasks")
242
+ if not isinstance(tasks, list):
243
+ return False
244
+ task_key = manifest.get("taskKey")
245
+ workflow = manifest.get("workflow") if isinstance(manifest.get("workflow"), dict) else {}
246
+ outcome = manifest.get("phaseOutcome") if isinstance(manifest.get("phaseOutcome"), dict) else {}
247
+ for entry in tasks:
248
+ if not isinstance(entry, dict) or entry.get("taskKey") != task_key:
249
+ continue
250
+ before = json.dumps(entry, sort_keys=True, ensure_ascii=False)
251
+ entry["currentPhase"] = workflow.get("currentPhase", "")
252
+ entry["currentPhaseState"] = workflow.get("currentPhaseState", "")
253
+ entry["lastCompletedPhase"] = workflow.get("lastCompletedPhase", "")
254
+ entry["nextRecommendedPhase"] = workflow.get("nextRecommendedPhase", "")
255
+ entry["latestReportPath"] = manifest.get("latestReportPath", "")
256
+ entry["latestRunStatus"] = manifest.get("latestRunStatus", entry.get("latestRunStatus", ""))
257
+ entry["currentStatus"] = manifest.get("currentStatus", entry.get("currentStatus", ""))
258
+ entry["phaseOutcome"] = outcome
259
+ if before == json.dumps(entry, sort_keys=True, ensure_ascii=False):
260
+ return False
261
+ _write_json(catalog_path, catalog)
262
+ return True
263
+ return False
264
+
265
+
266
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
267
+ path.parent.mkdir(parents=True, exist_ok=True)
268
+ path.write_text(
269
+ json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
270
+ encoding="utf-8",
271
+ )
@@ -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),