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.
- package/README.kr.md +5 -5
- package/README.md +5 -5
- package/docs/kr/architecture/storage-model.md +8 -3
- package/docs/kr/architecture.md +12 -13
- package/docs/kr/cli.md +4 -4
- package/docs/kr/container.md +1 -1
- package/docs/kr/performance-improvement-plan-v2.md +2 -2
- package/docs/project-structure-overview.md +2 -2
- package/package.json +7 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +12 -0
- package/runtime/bin/okstra-antigravity-exec.sh +1 -2
- package/runtime/bin/okstra-codex-exec.sh +1 -2
- package/runtime/bin/okstra-render-final-report.py +2 -9
- package/runtime/bin/okstra-spawn-followups.py +4 -6
- package/runtime/bin/okstra-trace-cleanup.sh +24 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +9 -11
- package/runtime/python/okstra_ctl/codex_dispatch.py +6 -13
- package/runtime/python/okstra_ctl/consumers.py +35 -0
- package/runtime/python/okstra_ctl/container.py +4 -6
- package/runtime/python/okstra_ctl/dispatch_core.py +8 -13
- package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
- package/runtime/python/okstra_ctl/handoff.py +2 -1
- package/runtime/python/okstra_ctl/implementation_outcome.py +271 -0
- package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
- package/runtime/python/okstra_ctl/path_hints.py +665 -0
- package/runtime/python/okstra_ctl/render.py +4 -3
- package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +7 -4
- package/runtime/python/okstra_ctl/run.py +3 -10
- package/runtime/python/okstra_ctl/run_context.py +4 -2
- package/runtime/python/okstra_ctl/stage_targets.py +18 -0
- package/runtime/python/okstra_ctl/wizard.py +37 -26
- package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
- package/runtime/python/okstra_project/state.py +40 -2
- package/runtime/python/okstra_token_usage/report.py +2 -5
- package/runtime/validators/validate-report-views.py +2 -1
- package/runtime/validators/validate-run.py +1 -11
- package/src/commands/lifecycle/install.mjs +5 -5
- package/src/commands/lifecycle/uninstall.mjs +4 -4
- package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
- package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
- package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
- package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
- package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
- package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
- package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
- package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
- package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
- package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
- package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
- package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
- package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
- package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
- package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
- package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
- package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
- package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
- package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
- package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
- package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
- package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
- package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
- package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
- package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
- package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
- package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
- package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
- package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
- package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
- package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
- package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
- package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
- package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
- package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
- package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
- package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
- package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
- package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
- package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
- package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
- package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
- package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
- package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
- package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
- package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
- package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
- package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
- package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
- package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
- package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
- package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
- package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
- package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
- package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
- package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
- package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
- package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
- package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
- package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
- package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
- package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
- package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
- package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
- package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
- package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
- package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
- package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
- package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
- package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
- package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
- package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
- package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
- package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
- package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
- package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
- package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
- package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
- package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
- package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
- package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
- package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
- package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
- package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
- package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
- package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
- package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
- package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
- package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
- package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
- package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
- package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
- package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
- package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
- package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
- package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
- package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
- package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
- package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
- package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
- package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
- package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
- package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
- package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
- package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
- package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
- package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
- 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
|
|
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
|
|
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
|
|
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>"
|
|
53
|
-
- On `아니오` / `n` / `keep` → leave
|
|
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
|
-
-
|
|
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` / `
|
|
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
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
consumers.done_rows,
|
|
141
137
|
anchor_base_commit=anchor,
|
|
142
138
|
candidate_base=head_sha,
|
|
143
139
|
project_root=Path(task_worktree_path),
|