okstra 0.172.0 → 0.174.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture/storage-model.md +24 -3
  3. package/docs/architecture.md +21 -35
  4. package/docs/cli.md +39 -7
  5. package/docs/container.md +1 -1
  6. package/docs/contributor-change-matrix.md +1 -1
  7. package/docs/performance-improvement-plan-v2.md +6 -5
  8. package/docs/project-structure-overview.md +33 -25
  9. package/docs/task-process/README.md +6 -4
  10. package/docs/task-process/error-analysis.md +2 -2
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation-option-selection.md +70 -0
  13. package/docs/task-process/implementation-planning.md +24 -16
  14. package/docs/task-process/requirements-discovery.md +2 -2
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/report-writer-worker.md +30 -6
  19. package/runtime/bin/lib/okstra/cli.sh +5 -1
  20. package/runtime/bin/lib/okstra/globals.sh +2 -1
  21. package/runtime/bin/lib/okstra/usage.sh +3 -0
  22. package/runtime/bin/okstra-provider-exec.py +29 -12
  23. package/runtime/bin/okstra-trace-cleanup.sh +58 -129
  24. package/runtime/bin/okstra.sh +2 -0
  25. package/runtime/prompts/duties/direction-selection-worker.md +44 -0
  26. package/runtime/prompts/duties/planning-worker.md +12 -4
  27. package/runtime/prompts/lead/adapters/cmux.md +2 -0
  28. package/runtime/prompts/lead/context-loader.md +1 -1
  29. package/runtime/prompts/lead/convergence.md +5 -5
  30. package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
  31. package/runtime/prompts/lead/plan-body-verification.md +23 -6
  32. package/runtime/prompts/lead/report-writer.md +33 -11
  33. package/runtime/prompts/profiles/_common-contract.md +3 -3
  34. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  35. package/runtime/prompts/profiles/_implementation-executor.md +2 -0
  36. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  37. package/runtime/prompts/profiles/error-analysis.md +4 -4
  38. package/runtime/prompts/profiles/final-verification.md +3 -3
  39. package/runtime/prompts/profiles/forbidden-actions.json +7 -0
  40. package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
  41. package/runtime/prompts/profiles/implementation-planning.md +61 -46
  42. package/runtime/prompts/profiles/implementation.md +4 -2
  43. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  44. package/runtime/prompts/profiles/release-handoff.md +1 -1
  45. package/runtime/prompts/profiles/requirements-discovery.md +3 -3
  46. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  47. package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
  48. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
  49. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
  50. package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
  51. package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
  52. package/runtime/python/okstra_ctl/conformance.py +68 -0
  53. package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
  54. package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
  55. package/runtime/python/okstra_ctl/doctor.py +2 -2
  56. package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
  57. package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
  58. package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
  59. package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
  60. package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
  61. package/runtime/python/okstra_ctl/implementation_options.py +479 -0
  62. package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
  63. package/runtime/python/okstra_ctl/plan_items.py +51 -3
  64. package/runtime/python/okstra_ctl/render.py +1 -0
  65. package/runtime/python/okstra_ctl/render_final_report.py +16 -19
  66. package/runtime/python/okstra_ctl/report_contract.py +45 -14
  67. package/runtime/python/okstra_ctl/report_finalize.py +68 -9
  68. package/runtime/python/okstra_ctl/report_html/render.py +4 -2
  69. package/runtime/python/okstra_ctl/report_html/router.py +4 -0
  70. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
  71. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
  72. package/runtime/python/okstra_ctl/report_views.py +148 -12
  73. package/runtime/python/okstra_ctl/run.py +393 -4
  74. package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
  75. package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
  76. package/runtime/python/okstra_ctl/session.py +69 -12
  77. package/runtime/python/okstra_ctl/team.py +51 -25
  78. package/runtime/python/okstra_ctl/tmux.py +19 -149
  79. package/runtime/python/okstra_ctl/user_response.py +75 -0
  80. package/runtime/python/okstra_ctl/wizard.py +144 -0
  81. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  82. package/runtime/python/okstra_ctl/worker_request.py +2 -0
  83. package/runtime/python/okstra_ctl/workflow.py +29 -7
  84. package/runtime/python/okstra_ctl/worktree.py +69 -3
  85. package/runtime/python/okstra_token_usage/cli.py +1 -1
  86. package/runtime/python/okstra_token_usage/collect.py +66 -6
  87. package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
  88. package/runtime/skills/okstra-setup/references/project-config.md +11 -0
  89. package/runtime/templates/reports/final-report-v2.template.md +4 -0
  90. package/runtime/templates/reports/final-verification-input.template.md +1 -1
  91. package/runtime/templates/reports/html/base.template.html +3 -2
  92. package/runtime/templates/reports/html/i18n/en.json +21 -1
  93. package/runtime/templates/reports/html/i18n/ko.json +21 -1
  94. package/runtime/templates/reports/html/macros/forms.html +21 -2
  95. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
  96. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
  97. package/runtime/templates/reports/i18n/en.json +13 -0
  98. package/runtime/templates/reports/implementation-input.template.md +4 -2
  99. package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
  100. package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
  101. package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
  102. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
  103. package/runtime/templates/reports/report.js +111 -4
  104. package/runtime/templates/reports/settings.template.json +0 -24
  105. package/runtime/templates/reports/task-brief.template.md +9 -3
  106. package/runtime/templates/reports/user-response.template.md +25 -4
  107. package/runtime/templates/worker-prompt-preamble.md +8 -0
  108. package/runtime/validators/lib/fixtures.sh +49 -17
  109. package/runtime/validators/validate-implementation-plan-stages.py +169 -4
  110. package/runtime/validators/validate-report-views.py +2 -2
  111. package/runtime/validators/validate-run.py +149 -498
  112. package/runtime/validators/validate_improvement_report.py +5 -1
  113. package/runtime/validators/validate_session_conformance.py +1 -1
  114. package/src/cli-registry.mjs +8 -1
  115. package/src/commands/execute/codex-run.mjs +1 -0
  116. package/src/commands/execute/render-bundle.mjs +1 -0
  117. package/src/commands/execute/team.mjs +3 -3
  118. package/src/commands/execute/worktree-status.mjs +109 -0
  119. package/src/commands/lifecycle/install.mjs +0 -2
  120. package/src/commands/report/finalize.mjs +13 -6
  121. package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
  122. package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
  123. package/runtime/templates/reports/final-report.template.md +0 -1258
@@ -102,12 +102,36 @@ You author the final-report data.json (the JSON SSOT). You author it against the
102
102
 
103
103
  The AI handoff Markdown is an agent-facing ledger: verdict, routing, clarification decisions, evidence, one structured task deliverable, and execution audits. The human HTML is the reader-facing explanation: `humanSummary` plus the selected task block's `userNarrative` and structured facts. Populate both human fields in data.json even though the Markdown intentionally omits their full prose. Worker discussion, convergence mechanics, and token usage belong to audit data and must not be copied into the HTML human main body.
104
104
 
105
+ ### Implementation-planning frontmatter contract
106
+
107
+ #### Selected-direction
108
+
109
+ Emit `frontmatter.approved` as `false` and copy `implementationPlanning.selectedDirectionRef.snapshotPath` into `frontmatter.selectedDirectionRef`. You MUST omit `frontmatter.implementationOption`; the direction was selected upstream and cannot be selected again in planning. `schemas/final-report-v2.0.schema.json` enforces the required selected-direction reference and rejects an `implementationOption` property for this branch.
110
+
111
+ #### Legacy candidate-comparison
112
+
113
+ Emit `frontmatter.approved` as `false` and `frontmatter.implementationOption` as the empty string `""`. The user later flips `approved` to `true` and fills `implementationOption` with the chosen Option Candidate name to authorise and scope the next `implementation` run. Every other report type follows the same empty `implementationOption` default; the schema's non-selected-direction branch requires that field and rejects a selected-direction reference.
114
+
115
+ ### General authoring rules
116
+
105
117
  Rules (the schema enforces most of these — they are listed here so you know *what* to populate, not *how* to validate):
106
118
 
107
119
  - Read the exact permitted header values from the task bundle schema excerpt. In the current v2 contract, `header.reportOwner` is `"Okstra lead"` and `header.reportAuthor` is `"Report writer worker"`. Set author to `"Okstra lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback. A legacy v1 excerpt may retain its historical compatibility values; follow that excerpt rather than inferring ownership from the provider.
108
120
  - **Source items (worker:item) preservation.** Every `consensus[].sourceItems`, `differences[].workersPosition[].itemId`, and `evidence.primary[].sourceItems` entry MUST carry the worker:item-id pair (e.g. `claude:F-001`, `codex:1.1`, `antigravity:F-3`, or `lead:mcp-1` for lead-only evidence). The schema enforces this via the `SourceItem` regex; bare worker-name lists no longer parse.
109
121
  - **Verdict Card consistency.** `verdictCard.verdictToken` and `verdictCard.direction` MUST byte-match `finalVerdict.verdictToken` / `.direction`; `validators/validate-run.py` diffs both and fails the run on divergence. `verdictCard.nextStep` names the same action as `finalVerdict.nextStep` and `recommendedNextSteps[0].text` but is written as the actionable command the reader runs (e.g. `/okstra-run task-key=… task-type=release-handoff`) where the other two are prose — it is deliberately not a byte copy. Duplicating the compared values across `verdictCard` and `finalVerdict` is intentional so the validator can diff them.
110
- - **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. When a candidate is a step in a propagation chain rather than a competing explanation — the analysis calls it a downstream step, a second stage, or a consequence of another candidate — set its `downstreamOf` to the ids of the candidates immediately upstream of it; leave the field absent for a candidate that stands on its own. Every id listed MUST be another candidate in the same report, no row may name itself, and the links MUST NOT form a cycle; `validators/validate-run.py::_validate_cause_chain` rejects all three. This is the only place the chain is machine-readable — prose calling a candidate "the second step of the chain" while `downstreamOf` is absent leaves the report's figure claiming the candidates are alternatives. Route `errorAnalysis.routing.nextTaskType=implementation-planning` with `direction=begin-planning`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row. Phase validation MUST enforce exact target agreement and uniqueness through `validators/validate-run.py::_validate_error_analysis_consistency`; until that check is implemented and executed, those semantics are contract requirements rather than enforced guarantees.
122
+ - **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. When a candidate is a step in a propagation chain rather than a competing explanation — the analysis calls it a downstream step, a second stage, or a consequence of another candidate — set its `downstreamOf` to the ids of the candidates immediately upstream of it; leave the field absent for a candidate that stands on its own. Every id listed MUST be another candidate in the same report, no row may name itself, and the links MUST NOT form a cycle; `validators/validate-run.py::_validate_cause_chain` rejects all three. This is the only place the chain is machine-readable — prose calling a candidate "the second step of the chain" while `downstreamOf` is absent leaves the report's figure claiming the candidates are alternatives. Route `errorAnalysis.routing.nextTaskType=implementation-option-selection` with `direction=begin-option-selection`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row; `validators/validate-run.py::_validate_error_analysis_consistency` enforces exact target agreement and uniqueness.
123
+ - **Implementation-option-selection comparison.** When `header.taskType` is `implementation-option-selection`, populate `implementationOptionSelection` from the converged direction-selection findings. Preserve every merged or rejected raw candidate in `candidateAudit`, and put at most three selectable candidates in `rankedOptions`. Each displayed candidate carries its requirement coverage, scope commitments, criterion scores, feasibility votes, safety blockers, unresolved feasibility facts, planning invariants, and exact coverage summary. In each displayed candidate, `expectedChangeAreas` names direction-level change surfaces, never exact file paths or an exact file list. `expectedVerification` names direction-level verification signals, never a stage list or executable test commands. `schemas/final-report-v2.0.schema.json` enforces the displayed-summary constants and the three-option cap; semantic recalculation belongs to `validators/validate-run.py`.
124
+ - **Implementation-planning direction branch.** For `planningContract: selected-direction`, read `selectedDirectionRef` and its snapshot, then preserve their core mechanism, architecture boundaries, and planning invariants in `directionRealization`. Materialize files, interfaces, stages, validation, rollback, and bidirectional original-requirement links without candidate generation, scoring, recommendation, or user candidate selection. Author exactly one `P-Dir-1` whose payload is the complete `directionRealization`; its verifier checks those preserved properties and any hidden direction change. If the direction must change, author `direction-invalidated` and no execution queue. Legacy candidate-comparison reruns retain Option Candidates, trade-offs, Recommended Option, and `P-Opt-*` semantics.
125
+
126
+ ```json
127
+ {
128
+ "candidateDetailBoundary": {
129
+ "expectedChangeAreas": "direction-level-only",
130
+ "expectedVerification": "direction-level-signals-only",
131
+ "forbidden": ["exact-file-lists", "stage-lists", "test-commands"]
132
+ }
133
+ }
134
+ ```
111
135
  - **Human narrative.** Populate required `humanSummary` and the selected task block's `userNarrative`. Human-visible analysis facts must not exist only in Markdown; HTML is derived independently and can use only data.json. Keep worker discussion and audit details in `crossVerification`, `executionStatus`, and `tokenUsage`, outside the human narrative fields.
112
136
  - **External QA advisory.** A Tier 3 entry requiring `db`, `http`, or
113
137
  `external` may be non-PASS without changing approval or final verdict. Render
@@ -116,7 +140,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
116
140
  and add the exact rerun command to `recommendedNextSteps`. Never turn this
117
141
  advisory alone into a clarification, Acceptance Blocker, conditional
118
142
  acceptance condition, or blocked routing.
119
- - **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. The schema `allOf` / `contains` clause enforces row presence, not exact route-target agreement or uniqueness; phase validation must enforce those error-analysis semantics as specified above.
143
+ - **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-option-selection` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. The schema `allOf` / `contains` clause enforces row presence, not exact route-target agreement or uniqueness; phase validation must enforce those error-analysis semantics as specified above.
120
144
  - **No deprecated sections.** The schema has no `4.5.8 User Approval Request` body field, no `4.5.9 Open Questions`, no `5.1 Additional Material Request`, no `5.2 User Confirmation Questions` — clarifications go under the unified `clarificationItems[]` array.
121
145
  - **Optional Section 0.** Include `clarificationCarryIn` ONLY when the lead's prompt provides a non-empty carry-in path. Omit the key entirely otherwise (do NOT set it to `null` or an empty object).
122
146
  - **Reading Confirmation** goes at `**Audit sidecar path:**` per the selected report-writer preamble's `Required reading` section — never in the data.json or the main worker-results file.
@@ -129,10 +153,10 @@ Rules (the schema enforces most of these — they are listed here so you know *w
129
153
  - Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
130
154
  - Preserve every analysis worker's ticket tagging — every row's `ticketId` field carries the ticket key or the task-fallback. For single-ticket runs, set `ticketCoverage` to `{"singleTicket": "<ticket>"}`. For runs that do not require ticket tagging (`release-handoff`, `final-verification`), set `ticketCoverage` to `{"omit": true}`.
131
155
  - For `requirements-discovery`, `error-analysis`, and `implementation-planning`, populate the top-level `endStateCoverage` with exactly one row per end-state id the brief declares — no more, no fewer. `disposition` is one of `addressed` / `deferred` / `not-applicable` / `blocked`. `addressed` requires a `coveredBy` anchor in THIS phase's own deliverable (requirements-discovery: the routing decision, the fan-out unit id, or the `C-NNN` clarification; error-analysis: the root-cause candidate or the next diagnostic; implementation-planning: the `R-NNN` row); every other disposition requires a `rationale`. Do not author a goal of your own here and do not restate the brief — this table records only how this phase accounted for what the reporter already pinned. When the brief declares no end-state ids (a brief authored before those sections existed), omit the field entirely. **Enforced:** `validators/validate-run.py` `_validate_end_state_coverage`.
132
- - For `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. A `covered` row's `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; use `documented-deviation` only when `coveredBy` states the concrete alternative and the row records non-empty unique `decisionRefs` plus `approvalDisposition`. Each `C-NNN` ref must name a clarification in this report; each `D-NNNN` ref must name a `decisionDrafts[].number`. `approvalDisposition: "accepted"` requires a referenced clarification with `status: answered|resolved` and non-empty `userInput`; `approvalDisposition: "blocked C-NNN"` requires that same-report clarification to be `status: open, blocks: approval`. Otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.ImplementationRequirementCoverageRow` and `validators/validate-run.py` `_validate_requirement_deviations`.
133
- - For `implementation-planning`, each `requirementCoverage` row's `source` is a graded cell, not prose — free text like `"carry-in from requirements-discovery C-001"` is rejected. Write exactly one of: `brief:EB-001` / `brief:PB-001` / `brief:EO-001`, an end-state id the brief declares — when the brief pins ids, citing a heading instead is rejected, because every brief carries the same generic headings and a heading cannot say WHICH reporter line the requirement came from (only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it); `derived:R-NNN — <one-line reason>`, whose chain must terminate at a `brief:` or `contract:` row of the same table without cycling; or `contract:<rule>`, for artifacts okstra's own phase contract mandates, whose allowlist is exactly the two tokens `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step) — any other rule name is rejected, so never invent one. (Maintainer SSOT for that allowlist: `scripts/okstra_ctl/scope_provenance.py` in the okstra repo.) A requirement you cannot source this way does not belong in the table: put it in `clarificationItems[]` with `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance`. In the same table, anchor every stage number in `coveredBy` to a `Stage` / `Stages` word (`Stage 2`, `Stages 1-3`) — `_validate_stage_has_requirement` reads that cell as prose and fails the plan when a Stage Map stage is cited by no row.
134
- - For `implementation-planning`, also populate `implementationPlanning.decisionDrafts` (one row per decision meeting all three decision-record criteria; `[]` otherwise) and `implementationPlanning.skippedAdrCandidates` (evaluated-but-dropped adr-candidates; `[]` otherwise). The schema excerpt enumerates the row shape; the renderer emits §5.4 `### Decision Drafts`. When `decisionDrafts` is non-empty, the plan's stages MUST carry a stepwise step that creates `.okstra/decisions/<NNNN>-<slug>.md` (validate-run gates this).
135
- - For `implementation-planning`, populate `implementationPlanning.variationPointAnalysis` — a `hasMultipleImplementations` judgement synthesized from the analysis workers' output, not a field filled in last. When it is `true`, write one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements that behavior), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`; when it is `false`, write a non-empty `noVariationRationale` and leave `points` empty (the two branches are mutually exclusive). Do NOT pass a boilerplate rationale — `false` is the cheaper field to fill, and a `false` declaration the brief or the sibling code in the workers' evidence contradicts is a `P-Var` DISAGREE, not a saving. Also populate `implementationPlanning.recommendedOption.testSeams`: one row per boundary a test injects at and replaces, each carrying `boundary` / `injectedAs` / `replacedInTest`. An empty list is a conscious "no seam needed" claim, never a default for a field nobody filled. The schema excerpt enumerates both row shapes — author against it. (Maintainer SSOT for these two rules: the `Required deliverable shape` bullet in `prompts/profiles/implementation-planning.md` in the okstra repo; that path is not resolvable here, so it is provenance, not a file to open.) **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` (the block is in `implementationPlanning.required`) plus `testSeams` in `$defs.RecommendedOption`'s `required`; `validators/validate-run.py` `_validate_variation_point_analysis` rejects a rationale-less `false`, a `false` carrying points, a `true` with no point, an `extract: true` decision leaving `interfaceKind` or `coveredBy` empty, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-*` plan item judged in §5.5.9.
156
+ - For selected-direction `implementation-planning`, preserve each original requirement ID and populate its `stageRefs`, `stepRefs`, `validationRefs`, and `fileRefs`; `validate_selected_direction_plan` enforces forward and reverse exact coverage. For legacy candidate-comparison `implementation-planning`, populate `implementationPlanning.requirementCoverage` with one row per concrete requirement from the brief / packet, using IDs `R-001`, `R-002`, ... in source order. A `covered` row's `coveredBy` MUST name the specific Option Candidate plus Stage/Step that satisfies the requirement. Use `status: "covered"` only when the report's plan actually covers it; use `documented-deviation` only when `coveredBy` states the concrete alternative and the row records non-empty unique `decisionRefs` plus `approvalDisposition`. Each `C-NNN` ref must name a clarification in this report; each `D-NNNN` ref must name a `decisionDrafts[].number`. `approvalDisposition: "accepted"` requires a referenced clarification with `status: answered|resolved` and non-empty `userInput`; `approvalDisposition: "blocked C-NNN"` requires that same-report clarification to be `status: open, blocks: approval`. Otherwise use `gap` or `blocked C-NNN` and ensure the corresponding `Clarification Items` row blocks approval. Do not collapse this into `ticketCoverage`; ticket coverage is not requirement coverage. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.ImplementationRequirementCoverageRow` and `validators/validate-run.py` `_validate_requirement_deviations`.
157
+ - For legacy candidate-comparison `implementation-planning`, each `requirementCoverage` row's `source` is a graded cell, not prose — free text like `"carry-in from requirements-discovery C-001"` is rejected. Write exactly one of: `brief:EB-001` / `brief:PB-001` / `brief:EO-001`, an end-state id the brief declares — when the brief pins ids, citing a heading instead is rejected, because every brief carries the same generic headings and a heading cannot say WHICH reporter line the requirement came from (only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it); `derived:R-NNN — <one-line reason>`, whose chain must terminate at a `brief:` or `contract:` row of the same table without cycling; or `contract:<rule>`, for artifacts okstra's own phase contract mandates, whose allowlist is exactly the two tokens `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step) — any other rule name is rejected, so never invent one. (Maintainer SSOT for that allowlist: `scripts/okstra_ctl/scope_provenance.py` in the okstra repo.) A requirement you cannot source this way does not belong in the table: put it in `clarificationItems[]` with `Blocks=approval`. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance`. In the same table, anchor every stage number in `coveredBy` to a `Stage` / `Stages` word (`Stage 2`, `Stages 1-3`) — `_validate_stage_has_requirement` reads that cell as prose and fails the plan when a Stage Map stage is cited by no row.
158
+ - For legacy candidate-comparison `implementation-planning`, also populate `implementationPlanning.decisionDrafts` (one row per decision meeting all three decision-record criteria; `[]` otherwise) and `implementationPlanning.skippedAdrCandidates` (evaluated-but-dropped adr-candidates; `[]` otherwise). The schema excerpt enumerates the row shape; the renderer emits §5.4 `### Decision Drafts`. When `decisionDrafts` is non-empty, the plan's stages MUST carry a stepwise step that creates `.okstra/decisions/<NNNN>-<slug>.md` (validate-run gates this).
159
+ - For `implementation-planning`, populate `implementationPlanning.variationPointAnalysis` — a `hasMultipleImplementations` judgement synthesized from the analysis workers' output, not a field filled in last. When it is `true`, write one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements that behavior), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`; when it is `false`, write a non-empty `noVariationRationale` and leave `points` empty (the two branches are mutually exclusive). Do NOT pass a boilerplate rationale. Populate test seams under `implementationPlanning.directionRealization.testSeams` for selected-direction plans and under `implementationPlanning.recommendedOption.testSeams` for legacy candidate-comparison plans. An empty list is a conscious "no seam needed" claim, never a default for a field nobody filled. Every point becomes a `P-Var-*` plan item judged in §5.5.9.
136
160
  - When the `Task Type` is `improvement-discovery`, populate `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative`. Each candidate carries the 11 logical fields enforced by `validators/validate_improvement_report.py`; each lens-coverage row records candidate IDs or an evidence-backed no-candidate rationale. Source IDs, lens names, and worker prefixes from `scripts/okstra_ctl/improvement_lenses.py`. The standard renderer derives the AI handoff Markdown; never author a free-form improvement report.
137
161
 
138
162
  Write the three completion artifacts and the separate audit sidecar with your `Write` tool — that is the canonical authoring path, and okstra ships no hook that blocks `.md` writes (its seeded settings carry no `PreToolUse` entry at all — only the session/subagent lifecycle hooks `SessionStart` compact-reminder, `SessionEnd` trace-cleanup, and `SubagentStop` / `TaskCompleted` pane reclaim, none of which can intercept a tool call). A Bash heredoc is acceptable ONLY when a specific `Write` call is genuinely rejected by the host environment, and it MUST produce byte-identical content — do not reach for it pre-emptively. After writing data.json, invoke the renderer (`Bash`): `okstra render-final-report <data.json path>`, then write the Worker Result Path pointer. Confirm data.json, rendered Markdown, the pointer, and the audit sidecar exist before responding with a short status line prefixed by your model identity, per the preamble §"Return message to the lead". **Enforced:** dispatch `completionPaths` requires the first three files and `validators/validate_session_conformance.py` validates the audit sidecar.
@@ -175,6 +175,10 @@ while [[ $# -gt 0 ]]; do
175
175
  CLARIFICATION_RESPONSE_PATH="$(require_option_value --clarification-response "${2-}")"
176
176
  shift 2
177
177
  ;;
178
+ --selected-direction)
179
+ SELECTED_DIRECTION_PATH="$(require_option_value --selected-direction "${2-}")"
180
+ shift 2
181
+ ;;
178
182
  --task-key)
179
183
  TASK_KEY_INPUT="$(require_option_value --task-key "${2-}")"
180
184
  shift 2
@@ -233,7 +237,7 @@ while [[ $# -gt 0 ]]; do
233
237
  printf ' hint: did you mean --task-id?\n' >&2
234
238
  ;;
235
239
  esac
236
- printf ' valid options: --render-only --resume-clarification --yes --workers --lead-provider --lead-model --claude-model --codex-model --antigravity-model --worker-model --report-writer-provider --report-writer-model --lead-runtime --executor --critic --related-tasks --work-category --task-type --project-id --project-root --task-group --task-id --task-brief --directive --base-ref --fix-cycle --clarification-response --task-key --approved-plan --approve --implementation-option --stage --stages --qa-waiver --no-plan-verification -h|--help\n' >&2
240
+ printf ' valid options: --render-only --resume-clarification --yes --workers --lead-provider --lead-model --claude-model --codex-model --antigravity-model --worker-model --report-writer-provider --report-writer-model --lead-runtime --executor --critic --related-tasks --work-category --task-type --project-id --project-root --task-group --task-id --task-brief --directive --base-ref --fix-cycle --clarification-response --selected-direction --task-key --approved-plan --approve --implementation-option --stage --stages --qa-waiver --no-plan-verification -h|--help\n' >&2
237
241
  usage
238
242
  exit 1
239
243
  ;;
@@ -3,7 +3,7 @@
3
3
  PROFILE_DIR="$WORKSPACE_ROOT/prompts/profiles"
4
4
  PROMPT_TEMPLATE="$WORKSPACE_ROOT/prompts/launch.template.md"
5
5
  TASK_INDEX_TEMPLATE="$WORKSPACE_ROOT/templates/project-docs/task-index.template.md"
6
- FINAL_REPORT_TEMPLATE_SOURCE="$WORKSPACE_ROOT/templates/reports/final-report.template.md"
6
+ FINAL_REPORT_TEMPLATE_SOURCE="$WORKSPACE_ROOT/templates/reports/final-report-v2.template.md"
7
7
  RUN_VALIDATOR_PATH="$WORKSPACE_ROOT/validators/validate-run.py"
8
8
  OKSTRA_ROOT=""
9
9
  OKSTRA_TASKS_ROOT=""
@@ -44,6 +44,7 @@ ANALYSIS_PROFILE=""
44
44
  DIRECTIVE=""
45
45
  FIX_CYCLE=""
46
46
  CLARIFICATION_RESPONSE_PATH=""
47
+ SELECTED_DIRECTION_PATH=""
47
48
  APPROVED_PLAN_PATH=""
48
49
  APPROVE_PLAN_ACK="false"
49
50
  # implementation 전용: 유저가 고른 Option Candidate 이름. 빈 값이면 implementation
@@ -43,6 +43,9 @@ optional arguments:
43
43
  input so the lead can reconcile each prior Q*. Use this for scripted or
44
44
  CI runs where the answer file is already prepared. Interactive users
45
45
  should prefer --resume-clarification, which wraps this flag.
46
+ --selected-direction Path to a validated implementation-option-selection final report.
47
+ Required for a new implementation-planning run. Existing planning
48
+ reruns continue to use --clarification-response with their prior report.
46
49
  --approved-plan Path to the approved final-report.md from a prior implementation-planning run.
47
50
  Required when --task-type=implementation; the file MUST contain a recorded user approval marker.
48
51
  --approve Treat the user's CLI invocation itself as the plan-approval signal. Only meaningful
@@ -40,10 +40,11 @@ from okstra_ctl.worker_runner import LIVE, QUIET, run_worker # noqa: E402
40
40
  _USAGE = (
41
41
  "usage: okstra-provider-exec.py <provider> <project-root> "
42
42
  "<model-execution-value> <prompt-path> [worktree-path] [role] "
43
- "[idle-timeout-seconds] [--presentation live|quiet]"
43
+ "[idle-timeout-seconds] [--presentation live|quiet] [--session-id <uuid>]"
44
44
  )
45
45
 
46
46
  _PRESENTATION_FLAG = "--presentation"
47
+ _SESSION_ID_FLAG = "--session-id"
47
48
  _PRESENTATIONS = (LIVE, QUIET)
48
49
 
49
50
 
@@ -66,6 +67,9 @@ class Invocation:
66
67
  def parse_invocation(argv: list[str]) -> Invocation:
67
68
  """Resolve the wrapper's positional contract into one runnable dispatch."""
68
69
  positional, presentation = _take_presentation(argv)
70
+ # Empty unless the dispatcher issued one. Without it the CLI picks its own
71
+ # id and nothing downstream can map that session back to this worker.
72
+ positional, session_id = _take_flag(positional, _SESSION_ID_FLAG, "")
69
73
  if not 4 <= len(positional) <= 7:
70
74
  raise PreflightError(64, _USAGE)
71
75
  provider_id, project_root_raw, model, prompt_raw = positional[:4]
@@ -90,6 +94,7 @@ def parse_invocation(argv: list[str]) -> Invocation:
90
94
  worktree_path=worktree,
91
95
  role=role,
92
96
  idle_timeout_seconds=idle_timeout_seconds,
97
+ session_id=session_id,
93
98
  )
94
99
  strategy = spec.exec_strategy
95
100
  _check_command(strategy, request)
@@ -103,27 +108,39 @@ def parse_invocation(argv: list[str]) -> Invocation:
103
108
  )
104
109
 
105
110
 
106
- def _take_presentation(argv: list[str]) -> tuple[list[str], str]:
107
- """Split the one flag out of an otherwise positional argv.
111
+ def _take_flag(argv: list[str], flag: str, default: str) -> tuple[list[str], str]:
112
+ """Split one value-carrying flag out of an otherwise positional argv.
108
113
 
109
- Defaults to ``quiet``. ``live`` is only ever right where a screen was
110
- declared, and the only callers that can declare one are the pane backends —
111
- which pass the flag explicitly. Defaulting the other way assumed a screen
112
- that a subagent dispatch does not have, and sent every worker's progress
113
- into its caller's context window instead.
114
+ The wrapper's contract is positional, so every flag it grows has to be
115
+ lifted out before the positions are counted. Written once because the flags
116
+ differ only in the value they carry: a second copy of this loop is where
117
+ their refusals of a flag with no value would drift apart.
114
118
  """
115
119
  positional: list[str] = []
116
- presentation = QUIET
120
+ value = default
117
121
  index = 0
118
122
  while index < len(argv):
119
- if argv[index] != _PRESENTATION_FLAG:
123
+ if argv[index] != flag:
120
124
  positional.append(argv[index])
121
125
  index += 1
122
126
  continue
123
127
  if index + 1 >= len(argv):
124
- raise PreflightError(64, f"{_PRESENTATION_FLAG} needs a value: {_USAGE}")
125
- presentation = argv[index + 1]
128
+ raise PreflightError(64, f"{flag} needs a value: {_USAGE}")
129
+ value = argv[index + 1]
126
130
  index += 2
131
+ return positional, value
132
+
133
+
134
+ def _take_presentation(argv: list[str]) -> tuple[list[str], str]:
135
+ """``_take_flag`` plus the allowlist only this flag has.
136
+
137
+ Defaults to ``quiet``. ``live`` is only ever right where a screen was
138
+ declared, and the only callers that can declare one are the pane backends —
139
+ which pass the flag explicitly. Defaulting the other way assumed a screen
140
+ that a subagent dispatch does not have, and sent every worker's progress
141
+ into its caller's context window instead.
142
+ """
143
+ positional, presentation = _take_flag(argv, _PRESENTATION_FLAG, QUIET)
127
144
  if presentation not in _PRESENTATIONS:
128
145
  allowed = " | ".join(_PRESENTATIONS)
129
146
  raise PreflightError(
@@ -1,45 +1,45 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # okstra-trace-cleanup.sh — close tmux panes created during okstra runs.
3
+ # okstra-trace-cleanup.sh — close the harness-owned worker panes of an okstra run.
4
4
  #
5
- # Worker-compute panes are tmux-pane backend siblings. Their dispatcher tags
6
- # each pane it owns with a pane-level user option (`@okstra_worker_run=<RUN_DIR>`),
7
- # so panes are found server-wide by tag — no tmux env var or pane-id registry is
8
- # needed, and the run-scoped tag keeps concurrent okstra runs from closing each
9
- # other's panes.
5
+ # The panes this closes are the teammate panes the HARNESS splits for dispatched
6
+ # worker agents. okstra does not create them and cannot tag them, so they are
7
+ # found by a title allowlist scoped to the lead's window. They accumulate until
8
+ # the pane budget runs out, and `shutdown_request` only idles the agent without
9
+ # freeing its pane — this script is the only thing that closes them, which is why
10
+ # the lead calls it at every round boundary and before every user gate.
10
11
  #
11
- # Trace panes were `tail -F` siblings the provider wrappers split, tagged
12
- # `@okstra_trace_run` / `@okstra_status`. Those wrappers are now four-line
13
- # entrypoints and worker progress renders into the worker's own pane, so
14
- # NOTHING SPAWNS A TRACE PANE and neither tag has a writer left. The trace
15
- # paths below still run and simply match nothing; `--reclaim-completed`, which
16
- # keys on `@okstra_status`, is inert for the same reason. Kept rather than
17
- # deleted because the hooks that call this script are already seeded on user
18
- # machines — retiring the trace machinery is a deliberate follow-up.
12
+ # Two tag-driven paths used to live here and both are gone:
19
13
  #
20
- # Two invocation shapes:
14
+ # - Worker-compute panes of the `tmux-pane` dispatch backend, tagged
15
+ # `@okstra_worker_run`. That backend was removed — a worker now gets a cmux
16
+ # surface or a cli-wrapper subprocess, and neither is a tmux pane okstra owns.
17
+ # - Trace panes, tagged `@okstra_trace_run` / `@okstra_status`, which the
18
+ # provider wrappers used to split as `tail -F` siblings. The wrappers became
19
+ # four-line entrypoints and worker progress renders into the worker's own
20
+ # pane, so nothing had spawned a trace pane for some time.
21
21
  #
22
- # --run-dir <RUN_DIR> Used by the LEAD between phases and at wrap-up. Closes
23
- # (a) trace panes tagged with this run's dir and
24
- # (b) worker-agent panes the harness gives to dispatched
25
- # subagents (`claude-worker` / `codex-worker` /
26
- # `antigravity-worker` / `report-writer-worker`), identified
27
- # by a title allowlist scoped to the LEAD's WINDOW. The
28
- # lead pane is read from `<RUN_DIR>/state/lead-pane.id`
29
- # (recorded once by the lead in its own foreground pane —
30
- # reliable even though Claude Code's Bash tool strips
31
- # `$TMUX`/`$TMUX_PANE`); it scopes the title scan and is
32
- # NEVER killed. Window scope keeps a second lead running
33
- # in another window of the same session out of range.
22
+ # With both writers gone the tag scan matched nothing, so it was removed along
23
+ # with the two modes that existed only to drive it (`--reap`, which the SessionEnd
24
+ # hook called, and `--reclaim-completed`, which `okstra-subagent-reclaim.sh` drove
25
+ # from SubagentStop / TaskCompleted). Those hooks are gone from the seeded
26
+ # settings too.
34
27
  #
35
- # --reap Used by the `SessionEnd` hook, where no single run-dir
36
- # applies. Closes every trace pane whose tag points under
37
- # `$CLAUDE_PROJECT_DIR/.okstra/` (or every tagged trace
38
- # pane if that env var is unset). Harness-owned
39
- # worker-agent panes are left to the harness.
28
+ # Invocation:
29
+ #
30
+ # --run-dir <RUN_DIR> Closes the run's worker-agent panes (`claude-worker` /
31
+ # `codex-worker` / `antigravity-worker` /
32
+ # `report-writer-worker` and the FleetView teammate
33
+ # prefixes), scoped to the LEAD's WINDOW. The lead pane is
34
+ # read from `<RUN_DIR>/state/lead-pane.id` (recorded once
35
+ # by the lead in its own foreground pane — reliable even
36
+ # though Claude Code's Bash tool strips `$TMUX` /
37
+ # `$TMUX_PANE`); it scopes the scan and is NEVER killed.
38
+ # Window scope keeps a second lead running in another
39
+ # window of the same session out of range.
40
40
  #
41
41
  # `--list` (alias `--dry-run`) prints `<pane_id>\t<pane_title>` per pane instead
42
- # of killing — only meaningful with `--run-dir`.
42
+ # of killing.
43
43
  #
44
44
  # `--keep <substr>` (repeatable) spares any pane whose current title contains
45
45
  # <substr>, in both the kill and the list set. Used to preserve an in-flight
@@ -50,7 +50,7 @@
50
50
 
51
51
  set -u
52
52
 
53
- # Trace-pane caller resolution helper (okstra_resolve_caller_pane) — see
53
+ # Caller-pane resolution helper (okstra_resolve_caller_pane) — see
54
54
  # lib/okstra/tmux-pane.sh. Used as the lead-pane fallback below so a missing /
55
55
  # stale lead-pane.id resolves to the pane THIS process actually runs in (via
56
56
  # ancestor-PID ↔ tmux pane_pid matching), never a foreign active-client pane.
@@ -58,43 +58,26 @@ set -u
58
58
  _clean_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
59
59
  [ -r "$_clean_script_dir/lib/okstra/tmux-pane.sh" ] && . "$_clean_script_dir/lib/okstra/tmux-pane.sh"
60
60
 
61
- # --reclaim-completed shells out to `python3 -m okstra_ctl.pane_reclaim`. The
62
- # package is a bin-sibling in the repo layout (scripts/okstra_ctl) and under
63
- # $OKSTRA_HOME/lib/python in the installed layout. Put both on PYTHONPATH so the
64
- # import resolves regardless of where this script runs from.
65
- _okstra_home="${OKSTRA_HOME:-$HOME/.okstra}"
66
- export PYTHONPATH="${_clean_script_dir}:${_okstra_home}/lib/python${PYTHONPATH:+:$PYTHONPATH}"
67
-
68
61
  MODE="kill" # kill | list
69
- RECLAIM=0 # 1: trace pane 은 @okstra_status 가 완료(exited)일 때만 회수 (--reclaim-completed)
70
- REAP=0
71
62
  run_dir=""
72
63
  KEEP_PATTERNS=() # --keep <substr>: panes whose title contains substr are spared from kill/list
73
64
  while [[ $# -gt 0 ]]; do
74
65
  case "$1" in
75
- --list|--dry-run) MODE="list" ;;
76
- --reclaim-completed) RECLAIM=1 ;;
77
- --reap) REAP=1 ;;
66
+ --list|--dry-run) MODE="list" ;;
78
67
  --run-dir) shift; run_dir="${1-}" ;;
79
68
  --run-dir=*) run_dir="${1#--run-dir=}" ;;
80
69
  --keep) shift; KEEP_PATTERNS+=("${1-}") ;;
81
70
  --keep=*) KEEP_PATTERNS+=("${1#--keep=}") ;;
82
71
  -h|--help)
83
72
  cat <<'USAGE'
84
- usage: okstra-trace-cleanup.sh (--run-dir <RUN_DIR> [--list] [--reclaim-completed] [--keep <substr>]... | --reap)
73
+ usage: okstra-trace-cleanup.sh --run-dir <RUN_DIR> [--list] [--keep <substr>]...
85
74
 
86
- --run-dir okstra run directory; closes that run's trace + worker-agent panes.
87
- --list with --run-dir: print "<pane_id>\t<pane_title>" per pane; no kill.
75
+ --run-dir okstra run directory; closes that run's worker-agent panes.
76
+ --list print "<pane_id>\t<pane_title>" per pane; no kill.
88
77
  --dry-run alias for --list.
89
78
  --keep <substr> exclude any pane whose title contains <substr> from the
90
79
  kill/list set (repeatable). Used to spare an in-flight
91
80
  report-writer at a round boundary.
92
- --reclaim-completed with --run-dir: restrict trace panes to those whose
93
- @okstra_status sidecar is terminal (stage=exited); in-flight
94
- and teammate panes are preserved. Skips the title-allowlist
95
- teammate scan. Combinable with --list.
96
- --reap close every okstra trace pane under $CLAUDE_PROJECT_DIR/.okstra
97
- (SessionEnd hook; no single run-dir applies).
98
81
  USAGE
99
82
  exit 0 ;;
100
83
  *)
@@ -104,29 +87,16 @@ USAGE
104
87
  shift
105
88
  done
106
89
 
107
- if [[ "$REAP" -eq 0 && -z "$run_dir" ]]; then
108
- printf 'okstra-trace-cleanup.sh: --run-dir <RUN_DIR> (or --reap) is required\n' >&2
90
+ if [[ -z "$run_dir" ]]; then
91
+ printf 'okstra-trace-cleanup.sh: --run-dir <RUN_DIR> is required\n' >&2
109
92
  exit 2
110
93
  fi
111
94
 
112
- # Canonicalize paths used in tag string-compares. The wrappers tag panes with
113
- # `pwd -P` (symlink-resolved), so the scope paths must be resolved the same way
114
- # — else a symlinked component (e.g. macOS /tmp -> /private/tmp) makes the
115
- # compare miss. Fall back to the literal value if the dir does not resolve.
116
- _resolve() { (cd "$1" 2>/dev/null && pwd -P) || printf '%s' "$1"; }
117
- [[ -n "$run_dir" ]] && run_dir="$(_resolve "$run_dir")"
118
- project_dir=""
119
- [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && project_dir="$(_resolve "$CLAUDE_PROJECT_DIR")"
120
-
121
- # Lead pane. For a run, prefer the value the lead recorded in its own foreground
122
- # pane; fall back to the active-pane probe. Rejected if the recorded pane is
123
- # gone. For --reap there is no run state — probe the active pane, used only to
124
- # avoid killing whatever pane the reap runs from.
95
+ # Lead pane. Prefer the value the lead recorded in its own foreground pane; fall
96
+ # back to the active-pane probe. Rejected if the recorded pane is gone.
125
97
  lead_pane=""
126
- if [[ "$REAP" -eq 0 ]]; then
127
- lead_pane_file="$run_dir/state/lead-pane.id"
128
- [[ -r "$lead_pane_file" ]] && lead_pane="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
129
- fi
98
+ lead_pane_file="$run_dir/state/lead-pane.id"
99
+ [[ -r "$lead_pane_file" ]] && lead_pane="$(head -n1 "$lead_pane_file" 2>/dev/null || true)"
130
100
  if [[ -z "$lead_pane" ]] || ! tmux display-message -p -t "$lead_pane" '#{pane_id}' >/dev/null 2>&1; then
131
101
  if type okstra_resolve_caller_pane >/dev/null 2>&1; then
132
102
  lead_pane="$(okstra_resolve_caller_pane 2>/dev/null || true)"
@@ -135,17 +105,6 @@ if [[ -z "$lead_pane" ]] || ! tmux display-message -p -t "$lead_pane" '#{pane_id
135
105
  fi
136
106
  fi
137
107
 
138
- # Does a trace pane's tag belong to the set we are closing?
139
- _tag_in_scope() {
140
- local tag="$1"
141
- if [[ "$REAP" -eq 1 ]]; then
142
- [[ -z "$tag" ]] && return 1
143
- [[ -n "$project_dir" ]] && { [[ "$tag" == "$project_dir/"* ]]; return; }
144
- return 0 # no project scope available → reap every tagged trace pane
145
- fi
146
- [[ "$tag" == "$run_dir" ]]
147
- }
148
-
149
108
  _title_in_okstra_scope() {
150
109
  local title="$1"
151
110
  case "$title" in
@@ -168,7 +127,6 @@ _title_in_okstra_scope() {
168
127
  }
169
128
 
170
129
  # A collected pane whose current title contains any --keep substring is spared.
171
- # Applied at the final emit so both the tag scan and the title scan honour it.
172
130
  _keep_excluded() {
173
131
  local pid="$1" title pat
174
132
  (( ${#KEEP_PATTERNS[@]} )) || return 1
@@ -181,54 +139,25 @@ _keep_excluded() {
181
139
 
182
140
  collect_okstra_panes() {
183
141
  local -a panes=()
184
- local pid trace_tag worker_tag status_tag title
185
-
186
- # (1) Trace and worker-compute panes tagged in scope — found server-wide by
187
- # tag, so no tmux env var or pane-id registry is needed. Each `@okstra_*`
188
- # column carries a leading `x` sentinel (stripped after read): tmux's `-F`
189
- # drops an UNSET user option AND its adjacent tab, which would otherwise
190
- # shift later columns left (e.g. an empty worker tag stealing the status
191
- # value). The sentinel keeps every column present so positional parsing holds.
192
- # `#x` strip 은 실제 태그 값을 깎지 않는다: 모든 태그 값(trace_run/worker_run/
193
- # status)은 절대경로라 `/` 로 시작 → `x` 로 시작하는 일이 없어 sentinel 만 벗겨진다.
194
- while IFS=$'\t' read -r pid trace_tag worker_tag status_tag; do
195
- trace_tag="${trace_tag#x}"; worker_tag="${worker_tag#x}"; status_tag="${status_tag#x}"
142
+ local pid title
143
+
144
+ # Title-allowlisted worker-agent panes in the lead's WINDOW. `list-panes -t
145
+ # <pane>` (no `-s`, no `-a`) resolves the window containing that pane and lists
146
+ # only that window's panes. Split-pane teammates always land in the lead's
147
+ # window, so this catches all of THIS run's worker panes while a second okstra
148
+ # lead in another window of the same tmux session is left untouched. Skipped
149
+ # when the lead pane is unknown.
150
+ [[ -n "$lead_pane" ]] || return 0
151
+ while IFS=$'\t' read -r pid title; do
196
152
  [[ -n "$pid" ]] || continue
197
153
  [[ "$pid" == "$lead_pane" ]] && continue
198
- if _tag_in_scope "$trace_tag" || _tag_in_scope "$worker_tag"; then
199
- if [[ "$RECLAIM" -eq 1 ]]; then
200
- # reclaim 모드: 완료(stage=exited)된 worker 의 pane 만. status 태그가
201
- # 없거나(teammate/비-wrapper pane) 미완료면 보존.
202
- [[ -n "$status_tag" ]] || continue
203
- python3 -m okstra_ctl.pane_reclaim "$status_tag" || continue
204
- fi
154
+ if _title_in_okstra_scope "$title"; then
205
155
  panes+=("$pid")
206
156
  fi
207
- done < <(tmux list-panes -a \
208
- -F '#{pane_id}'$'\t''x#{@okstra_trace_run}'$'\t''x#{@okstra_worker_run}'$'\t''x#{@okstra_status}' \
209
- 2>/dev/null || true)
210
- # (2) Title-allowlisted worker-agent panes in the lead's WINDOW. Only for a
211
- # run (reap leaves these harness-owned panes to the harness). `list-panes -t
212
- # <pane>` (no `-s`, no `-a`) resolves the window containing that pane and
213
- # lists only that window's panes. Split-pane teammates always land in the
214
- # lead's window, so this catches all of THIS run's worker panes while a second
215
- # okstra lead in another window of the same tmux session — whose in-flight
216
- # worker panes are untagged and title-only — is left untouched. Skipped when
217
- # the lead pane is unknown. reclaim 모드는 teammate pane 을 회수하지 않으므로
218
- # (완료 판정 불가, trace-only) 이 스캔을 건너뛴다.
219
- if [[ "$REAP" -eq 0 && "$RECLAIM" -eq 0 && -n "$lead_pane" ]]; then
220
- while IFS=$'\t' read -r pid title; do
221
- [[ -n "$pid" ]] || continue
222
- [[ "$pid" == "$lead_pane" ]] && continue
223
- if _title_in_okstra_scope "$title"; then
224
- panes+=("$pid")
225
- fi
226
- done < <(tmux list-panes -t "$lead_pane" \
227
- -F '#{pane_id}'$'\t''#{pane_title}' 2>/dev/null || true)
228
- fi
157
+ done < <(tmux list-panes -t "$lead_pane" \
158
+ -F '#{pane_id}'$'\t''#{pane_title}' 2>/dev/null || true)
229
159
 
230
- # Dedupe — a live trace pane can match both the tag scan and the title scan.
231
- # Then drop any pane a --keep pattern spares (in-flight report-writer).
160
+ # Drop any pane a --keep pattern spares (in-flight report-writer).
232
161
  if (( ${#panes[@]} )); then
233
162
  printf '%s\n' "${panes[@]}" | awk 'NF && !seen[$0]++' | while IFS= read -r _pid; do
234
163
  _keep_excluded "$_pid" && continue
@@ -187,6 +187,7 @@ okstra execution summary:
187
187
  task brief: ${BRIEF_PATH}
188
188
  directive: ${DIRECTIVE:-None}
189
189
  clarification response: ${CLARIFICATION_RESPONSE_PATH:-None}
190
+ selected direction: ${SELECTED_DIRECTION_PATH:-None}
190
191
  workers override: ${WORKERS_OVERRIDE:-None}
191
192
  executor (implementation only): ${EXECUTOR_OVERRIDE:-default(claude)}
192
193
  approved plan: ${APPROVED_PLAN_PATH:-None}
@@ -237,6 +238,7 @@ PY_ARGS=(
237
238
  [[ "$APPROVE_PLAN_ACK" == "true" ]] && PY_ARGS+=(--approve)
238
239
  [[ -n "${IMPLEMENTATION_OPTION-}" ]] && PY_ARGS+=(--implementation-option "$IMPLEMENTATION_OPTION")
239
240
  [[ -n "${CLARIFICATION_RESPONSE_PATH-}" ]] && PY_ARGS+=(--clarification-response "$CLARIFICATION_RESPONSE_PATH")
241
+ [[ -n "${SELECTED_DIRECTION_PATH-}" ]] && PY_ARGS+=(--selected-direction "$SELECTED_DIRECTION_PATH")
240
242
  [[ -n "${WORK_CATEGORY-}" ]] && PY_ARGS+=(--work-category "$WORK_CATEGORY")
241
243
  [[ -n "${BASE_REF-}" ]] && PY_ARGS+=(--base-ref "$BASE_REF")
242
244
  [[ -n "${STAGE-}" ]] && PY_ARGS+=(--stage "$STAGE")
@@ -0,0 +1,44 @@
1
+ ---
2
+ id: direction-selection-worker
3
+ version: 1
4
+ kind: role
5
+ appliesTo: direction-selection-worker
6
+ ---
7
+
8
+ # Direction Selection Worker Duty Contract
9
+
10
+ ## Responsibility
11
+
12
+ Compare feasible directions before planning in `candidate-comparison` mode, or validate one preselected direction in `preselected-validation` mode.
13
+
14
+ ## Required conduct
15
+
16
+ In `candidate-comparison` mode, inspect the evidence needed to distinguish candidates, submit no more than three candidates, state the strongest counterevidence for each one, and map every candidate to the stable brief end-state IDs it satisfies, preserves, or leaves unresolved. In `preselected-validation` mode, validate the one preselected direction against that evidence and mapping; the worker must not generate new candidates.
17
+
18
+ ## Decision principles
19
+
20
+ Score candidates against the same stated criteria. Prefer evidence-backed feasibility over familiarity, and preserve a rejected candidate when its evidence or trade-off could affect the later planning decision.
21
+
22
+ ## Authority and boundaries
23
+
24
+ Select directions only. Do not edit project state, author detailed file lists, create stage maps, prescribe execution commands, or approve an implementation plan.
25
+
26
+ ## Evidence standard
27
+
28
+ Each candidate, score, counterexample, and requirement mapping cites inspected evidence. State uncertainty when the code or brief cannot establish a criterion.
29
+
30
+ ## Collaboration contract
31
+
32
+ Reason independently from other workers. Do not collapse overlapping candidates or seek agreement before convergence; provide the evidence that lets the lead merge and re-evaluate them.
33
+
34
+ ## Completion criteria
35
+
36
+ In `candidate-comparison` mode, every submitted candidate has a criterion score, counterevidence, and stable requirement mapping. In `preselected-validation` mode, the one preselected direction has a validation result, counterevidence, and stable requirement mapping. Rejected candidates retain their audit reason and evidence.
37
+
38
+ ## Forbidden conduct
39
+
40
+ Do not turn a candidate into a detailed implementation plan, invent a requirement ID, omit contrary evidence, present a selection as user approval, or generate a new candidate in `preselected-validation` mode.
41
+
42
+ ## Blocked-state reporting
43
+
44
+ Name the missing evidence or unresolved requirement that prevents a candidate comparison, the inspection attempted, and the criterion it leaves unscored.
@@ -9,11 +9,19 @@ appliesTo: planning-worker
9
9
 
10
10
  ## Responsibility
11
11
 
12
- Produce an implementation direction a person can approve: feasible options with their trade-offs, one recommendation, and stages that carry the requirement to a verifiable end — all without writing the implementation.
12
+ Produce an executable implementation plan without writing the implementation.
13
+
14
+ ### Selected-direction responsibility
15
+
16
+ When `selected-direction.json` is present, read it and the requirements ledger first. Preserve the selected direction's core mechanism, architecture boundaries, user constraints, and planning invariants. Concretize only its files, interfaces, stages, validation, and rollback. Link every file and stage back to original requirements, and link every original requirement forward to its files, stages, and checks. If current code evidence requires changing the direction, return `direction-invalidated` with evidence and stop. Direction selection remains upstream of this branch.
17
+
18
+ ### Legacy candidate-comparison compatibility
19
+
20
+ For a legacy rerun without `selected-direction.json`, retain Option Candidates, trade-offs, the Recommended Option, and `P-Opt-*` verification semantics. Only this compatibility branch compares alternatives or chooses a recommendation.
13
21
 
14
22
  ## Required conduct
15
23
 
16
- Read the current state of the code the work touches before drafting options; compare at least two feasible options on evidence from that code unless the decision is already settled upstream; tie the recommendation to the trade-off that decides it; split the work into stages along real dependencies with each stage's validation signal and rollback; and connect every requirement to the stage that satisfies it.
24
+ Read the current state of the code the work touches before planning. Split work into stages along real dependencies with each stage's validation signal and rollback. Connect every requirement to the stage that satisfies it. In the selected-direction branch, verify direction preservation before adding plan detail. In the legacy branch, compare options on current-code evidence.
17
25
 
18
26
  ## Decision principles
19
27
 
@@ -29,11 +37,11 @@ Every cited path, symbol, and command must exist as written and be executable in
29
37
 
30
38
  ## Collaboration contract
31
39
 
32
- Draft independently of the other planners rather than converging on the first option proposed. Leave the choice between competing plans and the resolution of contested items to convergence and the lead, and hand the executor a plan complete enough to follow without re-deriving the decisions behind it.
40
+ Draft independently of the other planners. Leave contested plan details to convergence and the lead, and hand the executor a plan complete enough to follow without re-deriving the selected direction.
33
41
 
34
42
  ## Completion criteria
35
43
 
36
- Options, trade-offs, the recommendation, the stages with their dependencies, validation and rollback, and requirement coverage are all present and mutually consistent; every unresolved decision is recorded as such rather than assumed; and no stage depends on work the plan never places.
44
+ For selected-direction planning, the snapshot and requirements ledger are preserved; files, interfaces, stages, validation, rollback, and requirements links are mutually consistent; planning invariants have evidence; and no hidden direction change is present. For legacy candidate-comparison compatibility, Option Candidates, trade-offs, the Recommended Option, and `P-Opt-*` semantics remain present. Every unresolved decision is recorded rather than assumed, and no stage depends on work the plan never places.
37
45
 
38
46
  ## Forbidden conduct
39
47