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.
- package/README.md +23 -9
- package/docs/architecture/storage-model.md +39 -65
- package/docs/architecture.md +68 -60
- package/docs/cli.md +40 -23
- package/docs/for-ai/skills/okstra-run.md +13 -34
- package/docs/performance-improvement-plan-v2.md +2 -2
- package/docs/pr-template-usage.md +1 -1
- package/docs/project-structure-overview.md +26 -22
- package/docs/task-process/README.md +4 -4
- package/docs/task-process/common-flow.md +12 -12
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/package.json +2 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/grok-worker.md +256 -0
- package/runtime/agents/workers/kimi-worker.md +256 -0
- package/runtime/agents/workers/report-writer-worker.md +12 -12
- package/runtime/bin/lib/okstra/cli.sh +13 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/usage.sh +17 -12
- package/runtime/bin/okstra-grok-exec.sh +5 -0
- package/runtime/bin/okstra-kimi-exec.sh +5 -0
- package/runtime/bin/okstra-provider-exec.py +235 -0
- package/runtime/bin/okstra-render-final-report.py +4 -4
- package/runtime/bin/okstra-render-report-views.py +100 -12
- package/runtime/bin/okstra.sh +3 -0
- package/runtime/prompts/lead/adapters/antigravity.md +48 -0
- package/runtime/prompts/lead/adapters/claude-code.md +13 -11
- package/runtime/prompts/lead/adapters/codex.md +7 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
- package/runtime/prompts/lead/report-writer.md +16 -12
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +16 -10
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +12 -12
- package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
- package/runtime/prompts/profiles/error-analysis.md +2 -0
- package/runtime/prompts/profiles/feature-analysis.md +2 -0
- package/runtime/prompts/profiles/final-verification.md +3 -1
- package/runtime/prompts/profiles/forbidden-actions.json +4 -4
- package/runtime/prompts/profiles/implementation-planning.md +3 -1
- package/runtime/prompts/profiles/implementation.md +2 -2
- package/runtime/prompts/profiles/improvement-discovery.md +6 -2
- package/runtime/prompts/profiles/project-analysis.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +7 -7
- package/runtime/prompts/profiles/requirements-discovery.md +2 -0
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
- package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
- package/runtime/python/okstra_ctl/lead_events.py +1 -1
- package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
- package/runtime/python/okstra_ctl/models.py +156 -8
- package/runtime/python/okstra_ctl/path_hints.py +9 -25
- package/runtime/python/okstra_ctl/paths.py +1 -1
- package/runtime/python/okstra_ctl/render.py +172 -74
- package/runtime/python/okstra_ctl/render_final_report.py +136 -28
- package/runtime/python/okstra_ctl/report_contract.py +124 -0
- package/runtime/python/okstra_ctl/report_finalize.py +1 -1
- package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
- package/runtime/python/okstra_ctl/report_html/common.py +86 -0
- package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
- package/runtime/python/okstra_ctl/report_html/models.py +59 -0
- package/runtime/python/okstra_ctl/report_html/render.py +76 -0
- package/runtime/python/okstra_ctl/report_html/router.py +40 -0
- package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
- package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
- package/runtime/python/okstra_ctl/report_views.py +15 -43
- package/runtime/python/okstra_ctl/run.py +276 -51
- package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
- package/runtime/python/okstra_ctl/team.py +2 -7
- package/runtime/python/okstra_ctl/wizard.py +194 -21
- package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
- package/runtime/python/okstra_ctl/workers.py +3 -1
- package/runtime/python/okstra_ctl/workflow.py +4 -2
- package/runtime/python/okstra_token_usage/__init__.py +1 -0
- package/runtime/python/okstra_token_usage/collect.py +32 -23
- package/runtime/python/okstra_token_usage/pricing.py +35 -3
- package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
- package/runtime/skills/okstra-run/SKILL.md +31 -42
- package/runtime/templates/prd/pr-body.template.md +1 -1
- package/runtime/templates/reports/final-report-v2.template.md +66 -0
- package/runtime/templates/reports/html/assets/base.css +41 -0
- package/runtime/templates/reports/html/assets/base.js +5 -0
- package/runtime/templates/reports/html/base.template.html +79 -0
- package/runtime/templates/reports/html/macros/forms.html +47 -0
- package/runtime/templates/reports/html/macros/layout.html +19 -0
- package/runtime/templates/reports/html/macros/visualizations.html +27 -0
- package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
- package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
- package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
- package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
- package/runtime/templates/reports/report.js +21 -4
- package/runtime/templates/reports/settings.template.json +4 -0
- package/runtime/templates/reports/task-brief.template.md +7 -7
- package/runtime/validators/validate-report-views.py +86 -4
- package/runtime/validators/validate-run.py +73 -15
- package/runtime/validators/validate_improvement_report.py +55 -0
- package/runtime/validators/validate_session_conformance.py +2 -1
- package/src/cli-registry.mjs +4 -4
- package/src/commands/execute/codex-dispatch.mjs +7 -10
- package/src/commands/execute/render-bundle.mjs +3 -3
- package/src/commands/execute/run.mjs +17 -52
- package/src/commands/execute/wizard.mjs +4 -1
- package/src/commands/lifecycle/doctor.mjs +6 -3
- package/src/commands/lifecycle/install.mjs +49 -21
- package/src/commands/report/finalize.mjs +2 -3
- package/src/commands/report/render-final-report.mjs +4 -2
- package/src/commands/report/render-views.mjs +8 -8
- package/src/lib/runtime-manifest.mjs +1 -1
- package/src/lib/runtime-resolver.mjs +2 -2
- 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.
|
|
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
|
|
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
|
|
76
|
-
- `<instruction-set>/final-report-template.md` — the
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
- **
|
|
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-
|
|
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-
|
|
123
|
-
- When the `Task Type` is `improvement-discovery`, populate
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
91
|
-
--lead-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
Report writer worker:
|
|
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,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
|
|
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.
|
|
61
|
-
"
|
|
62
|
-
"
|
|
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(
|