okstra 0.146.1 → 0.148.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 (137) hide show
  1. package/README.md +23 -9
  2. package/docs/architecture/storage-model.md +39 -65
  3. package/docs/architecture.md +68 -60
  4. package/docs/cli.md +40 -23
  5. package/docs/for-ai/skills/okstra-run.md +13 -34
  6. package/docs/performance-improvement-plan-v2.md +2 -2
  7. package/docs/pr-template-usage.md +1 -1
  8. package/docs/project-structure-overview.md +26 -22
  9. package/docs/task-process/README.md +4 -4
  10. package/docs/task-process/common-flow.md +12 -12
  11. package/docs/task-process/final-verification.md +2 -2
  12. package/docs/task-process/implementation.md +1 -1
  13. package/docs/task-process/release-handoff.md +1 -1
  14. package/package.json +2 -2
  15. package/runtime/BUILD.json +2 -2
  16. package/runtime/agents/workers/antigravity-worker.md +2 -2
  17. package/runtime/agents/workers/claude-worker.md +1 -1
  18. package/runtime/agents/workers/codex-worker.md +2 -2
  19. package/runtime/agents/workers/grok-worker.md +256 -0
  20. package/runtime/agents/workers/kimi-worker.md +256 -0
  21. package/runtime/agents/workers/report-writer-worker.md +12 -12
  22. package/runtime/bin/lib/okstra/cli.sh +13 -1
  23. package/runtime/bin/lib/okstra/globals.sh +3 -0
  24. package/runtime/bin/lib/okstra/usage.sh +17 -12
  25. package/runtime/bin/okstra-grok-exec.sh +5 -0
  26. package/runtime/bin/okstra-kimi-exec.sh +5 -0
  27. package/runtime/bin/okstra-provider-exec.py +235 -0
  28. package/runtime/bin/okstra-render-final-report.py +4 -4
  29. package/runtime/bin/okstra-render-report-views.py +100 -12
  30. package/runtime/bin/okstra.sh +3 -0
  31. package/runtime/prompts/lead/adapters/antigravity.md +48 -0
  32. package/runtime/prompts/lead/adapters/claude-code.md +13 -11
  33. package/runtime/prompts/lead/adapters/codex.md +7 -7
  34. package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
  35. package/runtime/prompts/lead/report-writer.md +16 -12
  36. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  37. package/runtime/prompts/profiles/_common-contract.md +16 -10
  38. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  39. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  40. package/runtime/prompts/profiles/_implementation-executor.md +12 -12
  41. package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
  42. package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
  43. package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
  44. package/runtime/prompts/profiles/error-analysis.md +2 -0
  45. package/runtime/prompts/profiles/feature-analysis.md +2 -0
  46. package/runtime/prompts/profiles/final-verification.md +3 -1
  47. package/runtime/prompts/profiles/forbidden-actions.json +4 -4
  48. package/runtime/prompts/profiles/implementation-planning.md +3 -1
  49. package/runtime/prompts/profiles/implementation.md +2 -2
  50. package/runtime/prompts/profiles/improvement-discovery.md +6 -2
  51. package/runtime/prompts/profiles/project-analysis.md +2 -0
  52. package/runtime/prompts/profiles/release-handoff.md +7 -7
  53. package/runtime/prompts/profiles/requirements-discovery.md +2 -0
  54. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  55. package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
  56. package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
  57. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  58. package/runtime/python/okstra_ctl/lead_events.py +1 -1
  59. package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
  60. package/runtime/python/okstra_ctl/models.py +156 -8
  61. package/runtime/python/okstra_ctl/path_hints.py +9 -25
  62. package/runtime/python/okstra_ctl/paths.py +1 -1
  63. package/runtime/python/okstra_ctl/render.py +172 -74
  64. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  65. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  66. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  67. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  68. package/runtime/python/okstra_ctl/report_html/common.py +86 -0
  69. package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
  70. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  71. package/runtime/python/okstra_ctl/report_html/render.py +76 -0
  72. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  73. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  74. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
  75. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
  76. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
  77. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
  78. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
  79. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
  80. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
  81. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  82. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
  83. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
  84. package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
  85. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  86. package/runtime/python/okstra_ctl/report_views.py +15 -43
  87. package/runtime/python/okstra_ctl/run.py +276 -51
  88. package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
  89. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  90. package/runtime/python/okstra_ctl/team.py +2 -7
  91. package/runtime/python/okstra_ctl/wizard.py +194 -21
  92. package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
  93. package/runtime/python/okstra_ctl/workers.py +3 -1
  94. package/runtime/python/okstra_ctl/workflow.py +4 -2
  95. package/runtime/python/okstra_token_usage/__init__.py +1 -0
  96. package/runtime/python/okstra_token_usage/collect.py +32 -23
  97. package/runtime/python/okstra_token_usage/pricing.py +35 -3
  98. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  99. package/runtime/skills/okstra-run/SKILL.md +31 -42
  100. package/runtime/templates/prd/pr-body.template.md +1 -1
  101. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  102. package/runtime/templates/reports/html/assets/base.css +41 -0
  103. package/runtime/templates/reports/html/assets/base.js +5 -0
  104. package/runtime/templates/reports/html/base.template.html +79 -0
  105. package/runtime/templates/reports/html/macros/forms.html +47 -0
  106. package/runtime/templates/reports/html/macros/layout.html +19 -0
  107. package/runtime/templates/reports/html/macros/visualizations.html +27 -0
  108. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  109. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  110. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  111. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  112. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  113. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  114. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  115. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  116. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  117. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  118. package/runtime/templates/reports/report.js +21 -4
  119. package/runtime/templates/reports/settings.template.json +4 -0
  120. package/runtime/templates/reports/task-brief.template.md +7 -7
  121. package/runtime/validators/validate-report-views.py +86 -4
  122. package/runtime/validators/validate-run.py +73 -15
  123. package/runtime/validators/validate_improvement_report.py +55 -0
  124. package/runtime/validators/validate_session_conformance.py +2 -1
  125. package/src/cli-registry.mjs +4 -4
  126. package/src/commands/execute/codex-dispatch.mjs +7 -10
  127. package/src/commands/execute/render-bundle.mjs +3 -3
  128. package/src/commands/execute/run.mjs +17 -52
  129. package/src/commands/execute/wizard.mjs +4 -1
  130. package/src/commands/lifecycle/doctor.mjs +6 -3
  131. package/src/commands/lifecycle/install.mjs +49 -21
  132. package/src/commands/report/finalize.mjs +2 -3
  133. package/src/commands/report/render-final-report.mjs +4 -2
  134. package/src/commands/report/render-views.mjs +8 -8
  135. package/src/lib/runtime-manifest.mjs +1 -1
  136. package/src/lib/runtime-resolver.mjs +2 -2
  137. package/src/lib/worker-agent-render.mjs +50 -0
@@ -22,9 +22,9 @@ tools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "TodoWrite", "WebFetch"
22
22
 
23
23
  ## Authority
24
24
 
25
- You are the canonical author of `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` for this run. Claude lead has explicitly delegated file-authorship to you. The lead reviews your output but does not write the file.
25
+ You are the canonical author of `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` for this run. The host-native Okstra lead has explicitly delegated file-authorship to you. The lead reviews your output but does not write the file.
26
26
 
27
- The data.json is the **single source of truth**. The renderer (`scripts/okstra-render-final-report.py`) produces the user-facing markdown (`final-report-<task-type>-<seq>.md`) deterministically from it. You do NOT hand-write the markdown. The markdown is regenerated whenever the data.json changes (Phase 7 token substitution, future re-renders).
27
+ The data.json is the **single source of truth** for two audiences. The renderer (`scripts/okstra-render-final-report.py`) produces the AI handoff Markdown (`final-report-<task-type>-<seq>.md`) deterministically from it. Phase 7 produces the human HTML (`final-report-<task-type>-<seq>.html`) through the task-specific HTML renderer. HTML is rendered directly from the data.json; it is not a presentation of the Markdown. You do NOT hand-write either derived artifact. Both are regenerated whenever the data.json changes.
28
28
 
29
29
  If you find yourself thinking "I'll just write the markdown directly" — stop. Write the data.json with your `Write` tool and let the renderer produce the markdown.
30
30
 
@@ -72,30 +72,30 @@ Before writing the data.json, you MUST:
72
72
 
73
73
  For the report writer specifically, the `## Inputs` list always includes:
74
74
 
75
- - `<instruction-set>/final-report-schema.json` — the **per-task-type excerpt** of the data.json schema (scoped to this run's task-type at prep time: other task-types' deliverable blocks and their unreachable `$defs` are stripped). This is the shape you must author. Read this, NOT the full `schemas/final-report-v1.0.schema.json` (it is not in the task bundle and its `schemas/...` path is not resolvable here). Validation still runs against the full schema post-hoc, so the excerpt never relaxes the contract. The excerpt is frozen at prep time and carries the okstra version it was cut from (`x-okstraCutFromVersion`); if the renderer rejects a field the excerpt told you to write, the runtime upgraded mid-run — the renderer's error names the version skew, and the **installed schema wins**.
76
- - `<instruction-set>/final-report-template.md` — the **phase-stripped** Jinja2 template the renderer uses (only this run's §4.x deliverable block remains). Read it to understand which data.json fields appear where in the rendered markdown; do NOT edit it, and do NOT pull the full `templates/reports/final-report.template.md` source.
75
+ - `<instruction-set>/final-report-schema.json` — the **per-task-type excerpt** of schema v2 (other task-types' deliverable blocks and unreachable `$defs` are stripped). This is the shape you must author. Read this, NOT the full `schemas/final-report-v2.0.schema.json` source outside the task bundle. Validation still runs against the installed full schema, so the excerpt never relaxes the contract. The excerpt is frozen at prep time and carries the okstra version it was cut from (`x-okstraCutFromVersion`); if the renderer rejects a field the excerpt told you to write, the installed schema wins.
76
+ - `<instruction-set>/final-report-template.md` — the AI handoff Markdown template. Read it to understand which IDs, routing fields, evidence, task deliverable, and audit blocks appear in the AI artifact; do NOT edit it, and do NOT use it as the human presentation contract.
77
77
  - `templates/reports/i18n/en.json` and `templates/reports/i18n/ko.json`.
78
78
  - Every analysis worker's result file under `worker-results/`.
79
79
  - `state/convergence-<task-type>-<seq>.json` (if present). When present, reproduce its `roundHistory[]`, `round2SkippedReason`, and `finalClassificationCounts` verbatim into the final report's Section 6 Round History sub-table — do not recompute from worker results.
80
80
  - `<instruction-set>/task-brief.md` — the brief this run was prepared from. The lead already lists it under `## Inputs`; what it is FOR is the `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` items, whose `EB-NNN` / `PB-NNN` / `EO-NNN` ids are exactly what `endStateCoverage` maps. Read the instruction-set copy, NOT the project's `.okstra/briefs/...` path from the task manifest — that path is not resolvable here, same as the full schema source. A report that omits an id the brief pinned is rejected by the run validator.
81
81
 
82
- For the carry-in `clarification-response.md` (if present), walk every row of `## 1. Clarification Items` including rows whose `User input` cell is blank — a blank cell with `Status=open` is a signal you must surface in the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it when the carry-in path is non-empty). When no carry-in path was provided, OMIT the `## 0.` heading entirely — do NOT write an empty-state stub.
82
+ For a carry-in `clarification-response.md`, reconcile every prior `clarificationItems[]` row, including an open row with blank user input. Record the current status and user decision in data.json; the AI handoff renderer places it under `## Clarification and User Decisions`, while HTML renders any still-open response controls. When no carry-in path was provided, omit `clarificationCarryIn` entirely.
83
83
 
84
84
  Write a Reading Confirmation block to `**Audit sidecar path:**`, per the selected report-writer preamble's `Required reading` section (the main final-report and worker-results files carry no Section 0 heading). If you cannot truthfully confirm a file end-to-end, record a `tool-failure` in the errors sidecar instead of fabricating the report.
85
85
 
86
86
  ## Authoring Contract
87
87
 
88
- You author the final-report data.json (the JSON SSOT). You author it against the `<instruction-set>/final-report-schema.json` excerpt — its `$defs` enumerate every row shape, enum value, and cross-field constraint that applies to this run's task-type. The validator and renderer both consume the **full** `schemas/final-report-v1.0.schema.json` (the excerpt is a faithful task-type-scoped subset of it), so a data.json that satisfies the excerpt is a data.json that validates and renders correctly.
88
+ You author the final-report data.json (the JSON SSOT). You author it against the `<instruction-set>/final-report-schema.json` excerpt — its `$defs` enumerate every row shape, enum value, and cross-field constraint that applies to this run's task-type. The validator and renderers both consume the **full** `schemas/final-report-v2.0.schema.json` (the excerpt is a faithful task-type-scoped subset of it), so a data.json that satisfies the excerpt can independently produce both audience artifacts.
89
89
 
90
- The rendered markdown (`final-report-<task-type>-<seq>.md`) is produced by `scripts/okstra-render-final-report.py` immediately after you write the data.json. The HTML view (`*.html`) is produced from the markdown by the lead's Phase 7 `okstra report-finalize` run, not by you. The data.json is the only file you write; the rest are derived.
90
+ 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.
91
91
 
92
92
  Rules (the schema enforces most of these — they are listed here so you know *what* to populate, not *how* to validate):
93
93
 
94
- - `header.reportAuthor` is `"Report writer worker"`; `header.reportOwner` is `"Claude lead"`. Set author to `"Claude lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback.
94
+ - 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.
95
95
  - **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.
96
96
  - **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.
97
97
  - **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. 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.
98
- - **Reader Summary.** Populate `readerSummary` when the schema excerpt exposes it. It is the human-first entrypoint for both Markdown and HTML: one sentence for the decision, one for the human action required, one for blockers, one for audit sections safe to skip on first read, and one runnable recommended command. Do not duplicate raw evidence tables here.
98
+ - **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.
99
99
  - **External QA advisory.** A Tier 3 entry requiring `db`, `http`, or
100
100
  `external` may be non-PASS without changing approval or final verdict. Render
101
101
  its command log row as `tier: 3`, `status: advisory`, keep the observed and
@@ -116,11 +116,11 @@ Rules (the schema enforces most of these — they are listed here so you know *w
116
116
  - Cite file paths and line numbers in every `evidence.primary[].source` / `consensus[].evidence` cell.
117
117
  - 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}`.
118
118
  - 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`.
119
- - 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-v1.0.schema.json` `$defs.ImplementationRequirementCoverageRow` and `validators/validate-run.py` `_validate_requirement_deviations`.
119
+ - 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`.
120
120
  - 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.
121
121
  - 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).
122
- - 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-v1.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.
123
- - When the `Task Type` is `improvement-discovery`, populate `## 5.9 Improvement Candidates` with the 11-column schema enforced by `validators/validate_improvement_report.py`. The `Expected behavior after` cell states in one observable sentence what becomes different once the candidate is applied — it seeds the downstream brief's `EB-NNN` / `EO-NNN`, and an empty cell fails the run. Source the row IDs (`I-NNN`), lens whitelist, and Source workers patterns from `scripts/okstra_ctl/improvement_lenses.py` — do NOT introduce new lens names or worker prefixes. `improvement-discovery` is NOT in the data.json schema enum, so author its markdown directly (not via `okstra-render-final-report.py`). Immediately after writing the markdown, run (`Bash`): `okstra inject-report-index <markdown path> --report-language <en|ko>`. That adds the top-of-report Index plus `I-NNN` / `C-NNN` scroll anchors; the run validator fails the report when the Index anchor is absent.
122
+ - 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.
123
+ - 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.
124
124
 
125
125
  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 only settings hook is the `SessionEnd` trace-cleanup; the coding-preflight hook emits reminders but never blocks). 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.
126
126
 
@@ -66,6 +66,10 @@ while [[ $# -gt 0 ]]; do
66
66
  LEAD_MODEL_OVERRIDE="$(require_option_value --lead-model "${2-}")"
67
67
  shift 2
68
68
  ;;
69
+ --lead-provider)
70
+ LEAD_PROVIDER_OVERRIDE="$(require_option_value --lead-provider "${2-}")"
71
+ shift 2
72
+ ;;
69
73
  --claude-model)
70
74
  CLAUDE_MODEL_OVERRIDE="$(require_option_value --claude-model "${2-}")"
71
75
  shift 2
@@ -78,6 +82,14 @@ while [[ $# -gt 0 ]]; do
78
82
  ANTIGRAVITY_MODEL_OVERRIDE="$(require_option_value --antigravity-model "${2-}")"
79
83
  shift 2
80
84
  ;;
85
+ --worker-model)
86
+ WORKER_MODELS_OVERRIDE="$(require_option_value --worker-model "${2-}")"
87
+ shift 2
88
+ ;;
89
+ --report-writer-provider)
90
+ REPORT_WRITER_PROVIDER_OVERRIDE="$(require_option_value --report-writer-provider "${2-}")"
91
+ shift 2
92
+ ;;
81
93
  --report-writer-model)
82
94
  REPORT_WRITER_MODEL_OVERRIDE="$(require_option_value --report-writer-model "${2-}")"
83
95
  shift 2
@@ -212,7 +224,7 @@ while [[ $# -gt 0 ]]; do
212
224
  printf ' hint: did you mean --task-id?\n' >&2
213
225
  ;;
214
226
  esac
215
- printf ' valid options: --render-only --resume-clarification --yes --workers --lead-model --claude-model --codex-model --antigravity-model --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
227
+ 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
216
228
  usage
217
229
  exit 1
218
230
  ;;
@@ -18,9 +18,12 @@ ASSUME_YES="false"
18
18
  RESUME_CLARIFICATION_MODE="false"
19
19
  WORKERS_OVERRIDE=""
20
20
  LEAD_MODEL_OVERRIDE=""
21
+ LEAD_PROVIDER_OVERRIDE=""
21
22
  CLAUDE_MODEL_OVERRIDE=""
22
23
  CODEX_MODEL_OVERRIDE=""
23
24
  ANTIGRAVITY_MODEL_OVERRIDE=""
25
+ WORKER_MODELS_OVERRIDE=""
26
+ REPORT_WRITER_PROVIDER_OVERRIDE=""
24
27
  REPORT_WRITER_MODEL_OVERRIDE=""
25
28
  LEAD_RUNTIME="claude-code"
26
29
  EXECUTOR_OVERRIDE=""
@@ -3,10 +3,10 @@
3
3
  usage() {
4
4
  cat >&2 <<USAGE_EOF
5
5
  usage:
6
- $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--workers worker1,worker2] [--lead-model <model>] [--claude-model <model>] [--codex-model <model>] [--antigravity-model <model>] [--report-writer-model <model>] [--lead-runtime claude-code|codex] [--executor claude|codex|antigravity] [--critic off|claude|codex|antigravity] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>] [--fix-cycle <yes|no>]
6
+ $DISPLAY_COMMAND_NAME [--render-only] [--yes] [--no-plan-verification] --task-type <task-type> [--workers worker1,worker2] [--lead-provider <provider>] [--lead-model <model>] [--worker-model provider=model,...] [--report-writer-provider <provider>] [--report-writer-model <model>] [--lead-runtime claude-code|codex|antigravity|external] [--executor claude|codex|antigravity] [--critic off|claude|codex|antigravity|grok|kimi] [--related-tasks taskA,taskB] --project-id <project-id> [--project-root <path>] --task-group <task-group> --task-id <task-id> --task-brief <brief-path> [--directive <directive>] [--fix-cycle <yes|no>]
7
7
 
8
8
  summary:
9
- $DISPLAY_TOOL_NAME prepares a task-keyed instruction bundle for Claude Code and launches an interactive Claude session by default.
9
+ $DISPLAY_TOOL_NAME prepares a task-keyed instruction bundle. The standalone launcher defaults to an interactive Claude session; supported in-host skills keep the current Claude Code, Codex, or Antigravity session as the native lead.
10
10
  The stable task identifier is composed of project-id + task-group + task-id.
11
11
 
12
12
  Skills, worker agents, and the codex wrapper are installed once per user under
@@ -75,7 +75,7 @@ optional arguments:
75
75
  workflow.nextRecommendedPhase). Explicit flags always win.
76
76
 
77
77
  options:
78
- --render-only Render the Claude handoff prompt only. Do not launch Claude.
78
+ --render-only Render the host-neutral lead handoff prompt only. Do not launch a session.
79
79
  --resume-clarification
80
80
  Interactive convenience mode that wraps --clarification-response.
81
81
  Locates the latest requirements-discovery or error-analysis
@@ -86,24 +86,27 @@ options:
86
86
  (--project-id/--task-group/--task-id or --task-key). Mutually
87
87
  exclusive with --clarification-response and --approved-plan.
88
88
  --yes Skip interactive prompting and confirmation. Requires all required arguments.
89
- --workers Comma-separated worker list for this run. Default: claude,codex,report-writer
90
- (Antigravity worker is optional; add \`antigravity\` explicitly, e.g. --workers claude,codex,antigravity,report-writer)
91
- --lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus
89
+ --workers Comma-separated worker list for this run. Default: claude,codex,report-writer.
90
+ Optional read-only providers: antigravity, grok, kimi.
91
+ --lead-provider Compatibility assertion for the lead assignment. Must match the native Claude Code, Codex, or Antigravity host.
92
+ --lead-model Model for the host-native lead. Default: the selected provider's lead policy.
92
93
  --claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
93
94
  --codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.6-sol
94
95
  --antigravity-model Model for Antigravity worker. Default: OKSTRA_DEFAULT_ANTIGRAVITY_MODEL or gemini-3.1-pro
96
+ --worker-model Provider-qualified worker override CSV, e.g. grok=grok-4.5,kimi=kimi-k3.
97
+ --report-writer-provider
98
+ Provider for report writer. Supported: claude, codex. Default: claude.
95
99
  --report-writer-model
96
100
  Model for report writer worker. Default: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or sonnet
97
- --lead-runtime Lead runtime adapter. Default: claude-code.
98
- codex is currently render-only and records Codex adapter
99
- metadata in prepared artifacts without dispatching workers.
101
+ --lead-runtime Lead runtime adapter. Default: claude-code. In-host runs use the
102
+ matching native lead; non-host providers use CLI wrappers.
100
103
  --executor Provider that performs the Executor role during --task-type=implementation.
101
104
  One of: claude | codex | antigravity. Default: OKSTRA_DEFAULT_EXECUTOR or claude.
102
105
  The Executor is the only worker allowed to mutate project files; the other two
103
106
  providers are dispatched as read-only verifiers regardless of this selection.
104
107
  Has no effect on other task types.
105
108
  --critic Provider for the opt-in Phase 5.6 critic pass (coverage gaps /
106
- acceptance devil's-advocate). One of: off | claude | codex | antigravity.
109
+ acceptance devil's-advocate). One of: off | claude | codex | antigravity | grok | kimi.
107
110
  Default: off.
108
111
  --related-tasks Optional comma-separated related task identifiers. Example: auth-token-refresh,frontend-login-ui
109
112
  --work-category Work-category classification for this task. One of:
@@ -121,11 +124,13 @@ options:
121
124
  -h, --help Show this help.
122
125
 
123
126
  model defaults:
124
- Claude lead: OKSTRA_DEFAULT_LEAD_MODEL or opus
125
- Report writer worker: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or Claude lead default
127
+ Host-native lead: provider policy (Claude default: opus; Codex default: gpt-5.6-sol)
128
+ Report writer worker: selected provider policy (Claude default: sonnet)
126
129
  Claude worker: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
127
130
  Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.6-sol
128
131
  Antigravity worker: OKSTRA_DEFAULT_ANTIGRAVITY_MODEL or gemini-3.1-pro
132
+ Grok worker: grok-build-0.1 (analyser) or grok-4.5 (critic)
133
+ Kimi worker: kimi-k2.7-code (analyser) or kimi-k3 (critic)
129
134
  Implementation executor: OKSTRA_DEFAULT_EXECUTOR or claude (one of: claude | codex | antigravity)
130
135
 
131
136
  output:
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ exec python3 "$script_dir/okstra-provider-exec.py" grok "$@"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ exec python3 "$script_dir/okstra-provider-exec.py" kimi "$@"
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env python3
2
+ """Run an external LLM CLI with the shared okstra wrapper contract."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import selectors
8
+ import shutil
9
+ import signal
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Callable
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ProviderCommand:
20
+ binary: str
21
+ wrapper: str
22
+ build_args: Callable[[str, str, str], list[str]]
23
+
24
+
25
+ def _grok_args(prompt: str, model: str, cwd: str) -> list[str]:
26
+ return [
27
+ "grok",
28
+ "-p",
29
+ prompt,
30
+ "-m",
31
+ model,
32
+ "--output-format",
33
+ "streaming-json",
34
+ "--cwd",
35
+ cwd,
36
+ ]
37
+
38
+
39
+ def _kimi_args(prompt: str, model: str, _cwd: str) -> list[str]:
40
+ return ["kimi", "-p", prompt, "-m", model, "--output-format", "stream-json"]
41
+
42
+
43
+ PROVIDERS = {
44
+ "grok": ProviderCommand("grok", "okstra-grok-exec.sh", _grok_args),
45
+ "kimi": ProviderCommand("kimi", "okstra-kimi-exec.sh", _kimi_args),
46
+ }
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Invocation:
51
+ provider: ProviderCommand
52
+ project_root: Path
53
+ model: str
54
+ prompt_path: Path
55
+ execution_root: Path
56
+ role: str
57
+ idle_timeout_seconds: int
58
+
59
+
60
+ class PreflightError(Exception):
61
+ def __init__(self, exit_code: int, message: str) -> None:
62
+ super().__init__(message)
63
+ self.exit_code = exit_code
64
+
65
+
66
+ def _parse_invocation(argv: list[str]) -> Invocation:
67
+ if len(argv) < 4 or len(argv) > 7:
68
+ raise PreflightError(
69
+ 64,
70
+ "usage: okstra-provider-exec.py <provider> <project-root> <model-execution-value> "
71
+ "<prompt-path> [worktree-path] [role] [idle-timeout-seconds]",
72
+ )
73
+ provider_id, project_root_raw, model, prompt_raw = argv[:4]
74
+ provider = PROVIDERS.get(provider_id)
75
+ if provider is None:
76
+ raise PreflightError(64, f"unsupported provider: {provider_id}")
77
+ worktree_raw = argv[4] if len(argv) >= 5 else ""
78
+ role = argv[5] if len(argv) >= 6 and argv[5] else "worker"
79
+ default_timeout = 1500 if role in {"executor", "verifier"} else 600
80
+ timeout_raw = argv[6] if len(argv) >= 7 else str(default_timeout)
81
+ return _validate_invocation(
82
+ provider, project_root_raw, model, prompt_raw, worktree_raw, role, timeout_raw
83
+ )
84
+
85
+
86
+ def _validate_invocation(
87
+ provider: ProviderCommand,
88
+ project_root_raw: str,
89
+ model: str,
90
+ prompt_raw: str,
91
+ worktree_raw: str,
92
+ role: str,
93
+ timeout_raw: str,
94
+ ) -> Invocation:
95
+ project_root = Path(project_root_raw)
96
+ prompt_path = Path(prompt_raw)
97
+ if not project_root_raw or not project_root.is_dir():
98
+ raise PreflightError(65, f"project-root is missing or not a directory: {project_root_raw!r}")
99
+ if not model:
100
+ raise PreflightError(66, "model-execution-value is empty")
101
+ if not prompt_raw or not prompt_path.is_file():
102
+ raise PreflightError(67, f"prompt-path is missing or not a file: {prompt_raw!r}")
103
+ if not timeout_raw.isdigit():
104
+ raise PreflightError(69, f"idle-timeout-seconds must be a non-negative integer: {timeout_raw!r}")
105
+ execution_root = Path(worktree_raw) if worktree_raw else project_root
106
+ if worktree_raw and not execution_root.is_dir():
107
+ raise PreflightError(68, f"worktree-path was provided but is not a directory: {worktree_raw!r}")
108
+ if shutil.which(provider.binary) is None:
109
+ raise PreflightError(127, f"{provider.binary} CLI is not installed on PATH")
110
+ return Invocation(
111
+ provider=provider,
112
+ project_root=project_root.resolve(),
113
+ model=model,
114
+ prompt_path=prompt_path.resolve(),
115
+ execution_root=execution_root.resolve(),
116
+ role=role,
117
+ idle_timeout_seconds=int(timeout_raw),
118
+ )
119
+
120
+
121
+ def _log_path(prompt_path: Path) -> Path:
122
+ if prompt_path.name.endswith(".md"):
123
+ return prompt_path.with_name(f"{prompt_path.name[:-3]}.log")
124
+ return Path(f"{prompt_path}.log")
125
+
126
+
127
+ def _write_status(path: Path, status: dict[str, object]) -> None:
128
+ temporary = Path(f"{path}.tmp")
129
+ temporary.write_text(json.dumps(status, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
130
+ os.replace(temporary, path)
131
+
132
+
133
+ def _terminate_process(process: subprocess.Popen[bytes]) -> None:
134
+ try:
135
+ os.killpg(process.pid, signal.SIGTERM)
136
+ except ProcessLookupError:
137
+ return
138
+ try:
139
+ process.wait(timeout=5)
140
+ except subprocess.TimeoutExpired:
141
+ try:
142
+ os.killpg(process.pid, signal.SIGKILL)
143
+ except ProcessLookupError:
144
+ pass
145
+ process.wait()
146
+
147
+
148
+ def _stream_process(
149
+ process: subprocess.Popen[bytes], log_file, idle_timeout_seconds: int
150
+ ) -> tuple[int, bool, int]:
151
+ selector = selectors.DefaultSelector()
152
+ assert process.stdout is not None
153
+ selector.register(process.stdout, selectors.EVENT_READ)
154
+ last_output = time.monotonic()
155
+ timed_out = False
156
+ idle_seconds = 0
157
+ while selector.get_map():
158
+ for key, _ in selector.select(timeout=0.25):
159
+ chunk = os.read(key.fd, 8192)
160
+ if not chunk:
161
+ selector.unregister(key.fileobj)
162
+ continue
163
+ last_output = time.monotonic()
164
+ sys.stdout.buffer.write(chunk)
165
+ sys.stdout.buffer.flush()
166
+ log_file.write(chunk)
167
+ log_file.flush()
168
+ idle_seconds = int(time.monotonic() - last_output)
169
+ if idle_timeout_seconds and idle_seconds >= idle_timeout_seconds and process.poll() is None:
170
+ timed_out = True
171
+ _terminate_process(process)
172
+ exit_code = process.wait()
173
+ return (124 if timed_out else exit_code), timed_out, idle_seconds
174
+
175
+
176
+ def _run(invocation: Invocation) -> int:
177
+ prompt = invocation.prompt_path.read_text(encoding="utf-8")
178
+ command = invocation.provider.build_args(prompt, invocation.model, str(invocation.execution_root))
179
+ status_path = Path(f"{invocation.prompt_path}.status.json")
180
+ log_path = _log_path(invocation.prompt_path)
181
+ started_ts = int(time.time())
182
+ started_monotonic = time.monotonic()
183
+ status: dict[str, object] = {
184
+ "schemaVersion": 1,
185
+ "wrapper": invocation.provider.wrapper,
186
+ "role": invocation.role,
187
+ "pid": os.getpid(),
188
+ "started_ts": started_ts,
189
+ "log_path": str(log_path),
190
+ "stage": "started",
191
+ }
192
+ _write_status(status_path, status)
193
+ with log_path.open("wb") as log_file:
194
+ process = subprocess.Popen(
195
+ command,
196
+ cwd=invocation.execution_root,
197
+ stdout=subprocess.PIPE,
198
+ stderr=subprocess.STDOUT,
199
+ start_new_session=True,
200
+ )
201
+ exit_code, timed_out, idle_seconds = _stream_process(
202
+ process, log_file, invocation.idle_timeout_seconds
203
+ )
204
+ ended_ts = int(time.time())
205
+ status.update(
206
+ stage="exited",
207
+ exit_code=exit_code,
208
+ ended_ts=ended_ts,
209
+ duration_ms=int((time.monotonic() - started_monotonic) * 1000),
210
+ )
211
+ if timed_out:
212
+ status.update(
213
+ timeout=True,
214
+ idle_at_ts=ended_ts,
215
+ idle_seconds=idle_seconds,
216
+ terminated_by="idle-watchdog",
217
+ )
218
+ _write_status(status_path, status)
219
+ return exit_code
220
+
221
+
222
+ def main(argv: list[str]) -> int:
223
+ try:
224
+ invocation = _parse_invocation(argv[1:])
225
+ return _run(invocation)
226
+ except PreflightError as exc:
227
+ print(f"okstra-provider-exec: {exc}", file=sys.stderr)
228
+ return exc.exit_code
229
+ except OSError as exc:
230
+ print(f"okstra-provider-exec: execution failed: {exc}", file=sys.stderr)
231
+ return 127 if isinstance(exc, FileNotFoundError) else 1
232
+
233
+
234
+ if __name__ == "__main__":
235
+ sys.exit(main(sys.argv))
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """CLI entry for the final-report renderer.
2
+ """CLI entry for the schema-selected AI handoff Markdown renderer.
3
3
 
4
4
  Usage:
5
5
  python3 scripts/okstra-render-final-report.py \\
@@ -57,9 +57,9 @@ def main(argv: list[str]) -> int:
57
57
  type=Path,
58
58
  default=None,
59
59
  help=(
60
- "Optional override for the Jinja2 template file. Default: "
61
- "$OKSTRA_HOME/templates/reports/final-report.template.md or "
62
- "the repo-local copy."
60
+ "Optional override for the Jinja2 template file. By default, "
61
+ "data.json.schemaVersion selects the matching installed or "
62
+ "repo-local report template."
63
63
  ),
64
64
  )
65
65
  parser.add_argument(