okstra 0.171.0 → 0.172.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/docs/architecture.md +13 -0
- package/docs/cli.md +4 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +3 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +4 -0
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +36 -12
- package/runtime/prompts/lead/plan-body-verification.md +22 -11
- package/runtime/prompts/lead/report-writer.md +11 -10
- package/runtime/prompts/lead/team-contract.md +2 -0
- package/runtime/prompts/profiles/_clarification-recommendation.md +3 -1
- package/runtime/prompts/profiles/_common-contract.md +2 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -1
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
- package/runtime/python/okstra_ctl/agent_activity.py +306 -0
- package/runtime/python/okstra_ctl/clarification_items.py +37 -20
- package/runtime/python/okstra_ctl/lead_events.py +47 -4
- package/runtime/python/okstra_ctl/render.py +11 -3
- package/runtime/python/okstra_ctl/report_finalize.py +51 -14
- package/runtime/python/okstra_ctl/report_html/common.py +5 -3
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +17 -1
- package/runtime/python/okstra_ctl/report_translation.py +14 -0
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +150 -0
- package/runtime/schemas/final-report-v2.0.schema.json +189 -0
- package/runtime/skills/okstra-user-response/SKILL.md +2 -2
- package/runtime/templates/reports/final-report-v2.template.md +8 -0
- package/runtime/templates/reports/html/assets/base.css +7 -0
- package/runtime/templates/reports/html/i18n/en.json +6 -1
- package/runtime/templates/reports/html/i18n/ko.json +6 -1
- package/runtime/templates/reports/html/macros/forms.html +21 -2
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +25 -0
- package/runtime/templates/reports/i18n/en.json +4 -0
- package/runtime/templates/reports/report.js +26 -17
- package/runtime/templates/reports/user-response.template.md +3 -1
- package/runtime/templates/worker-prompt-preamble.md +8 -0
- package/runtime/validators/validate-run.py +989 -29
- package/runtime/validators/validate_session_conformance.py +523 -35
- package/src/cli-registry.mjs +7 -0
- package/src/commands/report/agent-activity.mjs +21 -0
package/docs/architecture.md
CHANGED
|
@@ -852,6 +852,18 @@ The `## 0. Reading Confirmation` block from worker output is written to the side
|
|
|
852
852
|
If there are no substantive differences, state that fact rather than manufacturing a contrast.
|
|
853
853
|
Write the actual Markdown report body to the file instead of metadata about save failures or session limitations.
|
|
854
854
|
|
|
855
|
+
### Implementation-planning activity and approval contract
|
|
856
|
+
|
|
857
|
+
The manifest-provided `lead-events-*.jsonl` file is the canonical record for structured agent activity. Activity writers append `eventType: "activity"` rows to that file and receive monotonically increasing `A-NNN` identifiers from the shared append path.
|
|
858
|
+
|
|
859
|
+
`activityContractVersion: 1` is an interpretation version for new `implementation-planning` artifacts. A run without that field remains a historical run and does not require activity events or an `agentActivity[]` projection.
|
|
860
|
+
|
|
861
|
+
The shared `okstra report-finalize` entrypoint projects canonical activity before translation source checking. Its in-process `project-activity` step filters events by run identity, validates activity ID order, and replaces only `agentActivity[]` in the report data. A legacy manifest without activity contract v1 leaves data.json unchanged. For a non-English report, the lead runs the finalizer with `--only project-activity --only check-source` before translator dispatch, then runs the full finalizer after the translation sidecar exists. Conformance compares the resulting `agentActivity[]` IDs, order, and core fields with the canonical events for every lead host.
|
|
862
|
+
|
|
863
|
+
Approval blockers use `open`, `answered`, `resolved`, and `obsolete`. Both `open` and `answered` block approval because `answered` means that a user response exists but has not passed application and checking. A response sidecar hides an answered row from the next user-input prompt, but it does not change the approval status of an existing report.
|
|
864
|
+
|
|
865
|
+
An approval row classifies its cause as `user-decision`, `noncritical-dissent`, or `correctness-critical`. A `noncritical-dissent` row can become `passed-with-dissent` only after the user explicitly selects `accept-risk` and the report records the related activity evidence. A `correctness-critical` row cannot use `accept-risk`; it becomes resolved only after the plan is revised and the affected target passes re-verification.
|
|
866
|
+
|
|
855
867
|
## Final report views (HTML)
|
|
856
868
|
|
|
857
869
|
The Phase 7 `render-views` step accepts either a final-report data.json or its Markdown sibling. For schema v2, it locates and validates `final-report-<task-type>-<seq>.data.json`, selects the task type fail-closed, and renders HTML directly from the structured data. It does not parse the AI Markdown back into a human model. The lead reaches this step through `okstra report-finalize`, which owns the shared Phase 7 sequence in `scripts/okstra_ctl/report_finalize.py`.
|
|
@@ -859,6 +871,7 @@ The Phase 7 `render-views` step accepts either a final-report data.json or its M
|
|
|
859
871
|
- `reports/final-report-<task-type>-<seq>.html` — always generated for schema v2 with one of ten dedicated task templates. It includes an accessible summary, task-specific prose, tables and inline SVG diagrams, evidence references, decisions, and next actions. CSS / JS are embedded inline with no external assets; print and no-JavaScript fallback content preserve the essential information.
|
|
860
872
|
- **Human summary**: `humanSummary` is the sole v2 top-level human summary contract. It is not copied into AI Markdown. Each task view decides how to present it together with the task deliverable instead of sharing a generic dashboard body.
|
|
861
873
|
- **Audit isolation**: worker execution, convergence, and token/cost material remain available for traceability but are subordinate to the user's findings and decisions. They never replace the task analysis narrative.
|
|
874
|
+
- **Implementation-planning activity**: activity-contract reports show each agent's task, summary, and outcome in the default view. Commands, exit codes, file-and-line evidence, and result paths remain inside expandable detail. Approval decision cards link to the relevant `id-A-NNN` activity anchors and preserve each option's disposition in the exported user response.
|
|
862
875
|
- **Schema v1 compatibility**: existing v1 data and quick Markdown reports keep the legacy conditional renderer, including `readerSummary`, reader modes, and the original Markdown-parsing path.
|
|
863
876
|
- **`C-*` select option order (schema v1)**: the legacy renderer parses `Expected form`, puts the `Recommended:` answer **first**, and relabels the `Alternatives:` items consecutively as `(a)`, `(b)`, and so on (the original character labels are not retained). Schema v2 parses nothing here — a `Kind=decision` row carries `options[]`, and the presentation order is the array order with the `role: recommended` entry first.
|
|
864
877
|
|
package/docs/cli.md
CHANGED
|
@@ -767,7 +767,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
767
767
|
| `okstra worker-audit-check --run-dir <runs/<task-type>/> --task-type <type> --seq <nnn> [--worker <id>]` | Apply the Phase 7 worker audit-sidecar rules mid-run, while the worker session is still alive. For each of this run's `worker-results/<worker>-<task-type>-<seq>.md` it checks that the file carries no `## 0. Reading Confirmation` heading, that the matching audit sidecar exists, and — for prompts carrying the required-v1 evidence-ledger marker — that every backticked `path:line` citation has an Evidence read row in that sidecar. `--worker` scopes it to the role that just returned. Emits `{ok, failures[]}` and exits 2 when `failures[]` is non-empty. The rules come from the `okstra_ctl.worker_audit_ledger` SSOT shared with `validate-run.py`, so an early pass and the Phase 7 pass cannot disagree. Run it right after collecting a result: the same failure at Phase 7 leaves only a retroactive edit, which breaks the audit chain, or a failed run |
|
|
768
768
|
| `okstra log-report [--project-root <dir>] [--cwd <dir>] [--top <N>] [--json]` | Read-only inventory of wrapper transcript `.log` files and their sibling prompt `.md` files. Each ranked entry preserves `path` / `sizeBytes` for compatibility and also reports `transcriptPath`, `transcriptBytes`, `promptPath`, `promptBytes`, and `transcriptToPromptRatio`; totals distinguish prompt bytes from transcript bytes and count paired files. Ranking remains transcript-size descending |
|
|
769
769
|
| `okstra recap <assemble\|record\|note> <task-root\|task-key> …` | Backend for the okstra-inspect `recap` facet. `assemble` is read-only and prints a JSON summary of phase transitions across a task's runs. `record --kind <summary\|qa> --mode <artifact\|code> --answer <text> [--question <text>] [--citation <path:line> …]` appends one line to `<task-root>/recap/recap-log.jsonl` and never mutates other artifacts. `note --kind <verification-evidence\|decision-draft\|analysis-note> --slug <topic> --purpose <text> --scope-note <text> (--body <markdown>\|--body-file <path>)` writes an agent-authored note to `<task-root>/notes/` and prints its path plus the `--clarification-response` argument for feeding it into a later run |
|
|
770
|
-
| `okstra user-response <list\|show\|write> …` | Backend for the `/okstra-user-response` skill: answer a task's open clarification questions in-session and write the response sidecar. `list --home <dir> --project <id> [--limit <n>]` finds reports with open questions; `show --report <md>` reads one report's questions; `write --report <md> --answers <json> [--plan-decision <json>] [--task-key <key>]` writes the sidecar; the plan decision carries `status` (`approved` / `revision-requested` / `rejected`) and a `reason` that is mandatory for the latter two.
|
|
770
|
+
| `okstra user-response <list\|show\|write> …` | Backend for the `/okstra-user-response` skill: answer a task's open clarification questions in-session and write the response sidecar. `list --home <dir> --project <id> [--limit <n>]` finds reports with open questions; `show --report <md>` reads one report's questions; `write --report <md> --answers <json> [--plan-decision <json>] [--task-key <key>]` writes the sidecar; the plan decision carries `status` (`approved` / `revision-requested` / `rejected`) and a `reason` that is mandatory for the latter two. A selected structured option preserves its exact gate `disposition` (`select`, `accept-risk`, `request-revision`, or `reject`); direct free text uses `answer`, and a request to re-ask uses `reframe`. A `reframe` is carried into the next run as a re-scoped brief. JSON output; exit 0 ok / 1 error |
|
|
771
771
|
| `okstra pr <template\|branches\|gen> …` | Backend for the okstra-pr-gen skill. Git-only—no project registration required. `template list\|show <name\|default>\|add --name <name> (--content <text>\|--file <path>)\|path` manages PR body templates under `~/.okstra/template/pr/` (bundled fallback `src/commands/pr/default.md`); `branches` recommends a base branch; `gen --base <ref> [--template <name\|default>]` emits a JSON bundle of the template plus `<base>..HEAD` commits and `<base>...HEAD` diffstat |
|
|
772
772
|
| `okstra migrate [--apply] [--cwd <dir>] [--quiet]` | One-time migration of the project artifact root from `.project-docs/okstra/` to `.okstra/`. It is a dry run by default; `--apply` performs the move with `git mv` in a Git worktree, removes an empty `.project-docs/`, and synchronizes the `<PROJECT>/CLAUDE.md` import line, `.gitignore`, the project's rows in `~/.okstra/{recent,active}.jsonl`, and `~/.okstra/worktrees/registry.json`. It exits 1 if `.okstra/` already exists or the legacy directory is absent. Scheduled for removal by the end of v0.x |
|
|
773
773
|
| `okstra task-list [--project-root <path>]` | Combine `list_project_tasks` and `read_latest_task` into JSON containing the task catalog and latest task |
|
|
@@ -786,7 +786,9 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
786
786
|
| `okstra codex-dispatch --project-root <dir> --run-manifest <path> [--workers <csv>] [--dry-run]` | Compatibility alias for `okstra worker-dispatch`; it no longer selects a Codex-only transport-agent path. |
|
|
787
787
|
| `okstra agent-prompt materialize\|verify\|record-dispatch\|link-result\|materialize-result\|complete\|verify-completion` | Internal invocation-contract CLI. `materialize` composes model assignment, functional duty, and task instructions; `verify` rejects identity, path, snapshot, assignment, source, or digest drift. Run-backed calls resolve `assignmentRef` from the manifest, enforce `authorizedPaths`, and reject real-path or symbolic-link escape. `record-dispatch` records a verified host-native specification before dispatch and `link-result` binds the accepted result; one result path belongs to one dispatch, so a corrective round retires the first attempt with `reject-result --dispatch-id <first> --superseded-by <corrective> --reason <text>` before the new link is accepted — the rejected row stays in `agentResultLinks` carrying `supersededBy` and `rejectionReason` rather than being deleted. Standalone calls are identified by `(purpose, invocationId)` under `.okstra/agent-invocations/<purpose>/`; they publish a canonical result envelope and publish the completion marker last. Consumers use only the `returnedBody` from `verify-completion`. Metadata contains exactly `catalogDigest`, `assignmentDigest`, `dutyDigest`, `instructionDigest`, and `promptDigest`; JSON inputs use UTF-8, sorted keys, compact separators, and no non-finite values, while duty files use versioned sorted-name/byte framing. Instruction sources use `{kind: project\|runtime, path: <relative POSIX path>}` and never persist an installed absolute runtime path. A published prompt is immutable, so re-running `materialize` with an edited instruction file fails as `existing_invocation_conflict`; `--replace-undispatched` is the one exit, for a call that failed a pre-dispatch gate and therefore ran nowhere — it covers a differing prompt and a differing metadata alike, since the two are published together and describe one call. It republishes prompt and metadata together, and it is verified rather than trusted — a row in `agentDispatches` or `workerDispatches` naming this `invocationId` refuses the replacement and names the dispatch that used it. |
|
|
788
788
|
| `okstra team dispatch --project-root <dir> --run-manifest <path> [--workers <csv>] [--jobs-file <path>] [--dry-run]` / `okstra team await --project-root <dir> --run-manifest <path> [--json]` / `okstra team teardown --project-root <dir> --run-manifest <path> [--dry-run] [--json]` | Read a `leadRuntime=external` run manifest and dispatch, await, or tear down tmux-pane workers. Default dispatch excludes report writer; Phase 6 selects it explicitly, and mixed analysis/report jobs are rejected. If a tmux pane cannot be created, gracefully degrade to the CLI wrapper and record the fallback in `workerDispatches[].degradedFrom` |
|
|
789
|
-
| `okstra
|
|
789
|
+
| `okstra agent-activity append --project-root <dir> --run-manifest <path> --kind <kind> --agent <id> --summary <text> --outcome <outcome> [--plan-item-id <id>]… [--evidence-ref <ref>]… [--command-record <json>]… [--result-path <path>] [--audit-sidecar <path>]` | Append one structured activity to the run manifest's `leadEventsPath`. `kind` accepts `worker-dispatched`, `worker-completed`, `verification-round-completed`, `self-fix-applied`, `user-decision-required`, or `user-decision-evaluated`. `outcome` accepts `pending`, `completed`, `failed`, `blocked`, or `resolved`. Repeated `--command-record` values and `Evidence command` rows from `--audit-sidecar` must contain exactly `command`, `cwd`, `exitCode`, and `outputSummary`; malformed or potentially secret-bearing evidence stops the append. The command requires `activityContractVersion: 1` and returns the assigned `activityId` in JSON. |
|
|
790
|
+
| `okstra agent-activity project --project-root <dir> --run-manifest <path> --data <data.json>` | Project this run's canonical activity events into `agentActivity[]`. The command preserves event order, rejects duplicate or decreasing activity IDs, and replaces no other report field. A historical manifest without `activityContractVersion: 1` returns an empty projection and leaves data.json unchanged. Normal Phase 7 execution reaches this behavior through `report-finalize`; use the standalone command only for diagnostics. |
|
|
791
|
+
| `okstra report-finalize --project-root <dir> --run-manifest <path> --report <final-report.md>` | Run the whole Phase 7 post-report sequence in its contractual order: `project-activity` → `check-source` → `token-usage` → `render-views` → `spawn-followups` → `validate-run`. Stops at the first non-zero exit and names the failing step, then prints a per-step `[ok]` / `[FAIL]` / `[skip]` summary on stderr so the outcome is legible without parsing the JSON payload. `project-activity` is an in-process byte-preserving no-op for legacy manifests without activity contract v1. Every step is idempotent, so re-running after a fix is safe — but `--only <step>` (repeatable) reruns just the named steps in contractual order. For a non-English report, run `--only project-activity --only check-source` before translator dispatch, then run the full sequence after the translation sidecar exists. This is the same code path (`scripts/okstra_ctl/report_finalize.py`) every lead adapter runs after its report-writer completes. `--workspace-root` is owned by the Node wrapper. Prefer this over invoking the six steps individually. |
|
|
790
792
|
| `okstra render-views <final-report.data.json\|final-report.md>` | The Phase 7 `render-views` step, runnable on its own. Schema v2 data is rendered directly (contract: `schemas/final-report-v2.0.schema.json`) into an always-generated, task-specific human HTML sibling while `templates/reports/final-report-v2.template.md` independently owns the AI handoff Markdown. Passing the Markdown sibling locates the same v2 data.json. Schema v1 and quick reports keep the legacy conditional renderer. The Node wrapper calls `scripts/okstra-render-report-views.py`; `validators/validate-report-views.py` verifies source/schema/template digests, required human fields, form controls, external assets, diagram/table ID parity, and Response ID parity |
|
|
791
793
|
| `okstra design-prep <list\|show\|write>` | Review AI-prepared implementation design requests, inspect their effective confirmed response, or append a confirmed user/wizard response without editing the planning report |
|
|
792
794
|
| `okstra wizard <init\|step\|render-args\|confirmation\|outcome> --state-file <path>` | Interactive input state machine for okstra-run, implemented by `okstra_ctl.wizard`. Seed a state file with `init`, then repeatedly call `step --answer <val>` to receive the next `Prompt` JSON. `--answer` is **required**; use `--no-submit` to peek at the next prompt without submitting a response. A `pick` with more choices than the host picker can display keeps `kind: "pick"` but adds `presentation: "numbered-text"`; render every option as a numbered Markdown list and submit the user's 1-based number, exact value, or exact label. Invalid, out-of-range, and ambiguous answers re-prompt without dropping choices. `render-args` returns the final `render-bundle` argument map, and `confirmation` returns the user echo block. On a completed wizard, `outcome` returns `renderArgs`, `persistActions`, and `confirmationText` together; project/global release-handoff PR-template persistence appears as `persistActions[].command == "config.set"`. For an `implementation` task type, `stage_pick` follows `approved_plan_pick` and selects the stage before `executor_pick`. The brief step appears only for entry task types—requirements-discovery, error-analysis, improvement-discovery, project-analysis, feature-analysis, and change-impact-analysis. Analysis inputs use `feature_evidence_pick` / `feature_evidence`, `project_evidence_pick` / `project_evidence`, and `analysis_target_pick` / `analysis_target`; a revision-requested report prioritizes its same-task, same-type rerun. Downstream lifecycle phases automatically carry the manifest brief, with a three-option `brief_carry` fallback when none is registered; `release-handoff` has no brief and enters multi-select `handoff_stage_pick` for eligible stage groups or the whole task |
|
|
@@ -37,11 +37,11 @@ okstra preflight --runtime claude-code --json
|
|
|
37
37
|
## Flow
|
|
38
38
|
|
|
39
39
|
1. **list**: `okstra user-response list --home <home> --project <projectId> --limit 3` → an array of `{taskKey, taskType, seq, reportPath, reportMtime, openBlockerCount, openApprovalCount, unreadable}` (`openBlockerCount` = open rows with `Blocks` in `{approval, next-phase}`; `openApprovalCount` = the `approval`-only subset). If the array is empty, stop with "no open clarification". A 3-option picker (top recommendations + the final option always "Enter directly" for pasting a `reportPath`/`task-key` directly). `unreadable:true` is a §1 format drift — flag it with `⚠` and do not proceed (do not fabricate rows).
|
|
40
|
-
2. **show (data fetch, not a presentation step)**: `okstra user-response show --report <reportPath>` → `rows[]` of `{id, kind, blocks, status, statement, expectedForm, options, contextRefs, resolvedRefs}`. Each `options[]` entry is `{role, answer, rationale, scopeImpact, addedWork, directionChange}`; `resolvedRefs` carries the `definition` of internal tokens such as `RB-002`/`§4.7`. A schema-v1 report has nowhere to record impact, so those
|
|
40
|
+
2. **show (data fetch, not a presentation step)**: `okstra user-response show --report <reportPath>` → `rows[]` of `{id, kind, blocks, status, statement, expectedForm, options, contextRefs, resolvedRefs}`. Each `options[]` entry is `{role, answer, rationale, scopeImpact, addedWork, directionChange, disposition}`; `resolvedRefs` carries the `definition` of internal tokens such as `RB-002`/`§4.7`. Activity-contract v1 approval options carry the gate action in `disposition`. A legacy schema-v1 report has nowhere to record impact or a gate action, so those fields arrive empty. Do not print `rows` at the user and **do not paste the raw `statement` as the question** — announce only `<N> open items — I'll go through them one at a time.`
|
|
41
41
|
3. **ask, one item at a time**: iterate the rows in report order, **one item per `AskUserQuestion` call**, headed `[n/N] C-014 — blocks: approval gate`. Per item:
|
|
42
42
|
- **Background first**, in the message text above the picker, 3–6 lines: (a) *Situation* — what the run was doing when it stopped here; (b) *What is undecided* — the fork, internal tokens expanded inline from `resolvedRefs[].definition`, plus what is stuck (`approval` → the approval gate stays shut and `implementation` cannot start; `next-phase` → the next phase cannot begin); (c) *What changes with your answer*. Source it from `resolvedRefs[].definition`, else **Read** the `§`/`path:line` in `contextRefs[]`; **never invent it** — say the report is silent instead. Close with `Source: C-014 — "<raw statement>"`.
|
|
43
43
|
- **Picker: the row's `options[]` plus `Enter directly`** — slots follow array order, the `role: recommended` entry first with its label suffixed `(Recommended)`, `Enter directly` always last. Each `label` is the option's `answer`; each `description` is `<rationale> — Scope: <scopeImpact> · Added work: <addedWork> · Direction: <directionChange>`, in that fixed order. Never fold the three axes into one phrase. An empty axis is written `not stated in the report` — never inferred. More than three entries: keep the recommended one plus the two alternatives whose `scopeImpact` differs most, and say how many were left out. Never mark anything but `recommended` as recommended.
|
|
44
|
-
- **Transcribe**: an `options[]` pick → `value` = that option's `answer` text
|
|
44
|
+
- **Transcribe**: an `options[]` pick → `value` = that option's `answer` text and `disposition` = that option's `disposition` (`answer` only when absent on a legacy option); `Enter directly` → the user's utterance verbatim, `disposition:"answer"`; free text asking for a re-ask → `disposition:"reframe"` (does not satisfy the approval gate). A question back from the user records nothing — **Read** the ref, explain, re-ask the same item with the same options. Echo `[n/N] C-014 → answer: …` and move on. Each item's JSON: `{id, kind, value, rationale?, disposition}`.
|
|
45
45
|
4. **echo → confirmed gate**: before `write`, echo the whole collection (each `id`·`disposition`·`value`·`rationale`·approval) as-is and get explicit confirmation. Never `write` before `confirmed`. On any change, re-echo and re-confirm.
|
|
46
46
|
5. **plan decision (optional)**: only when the user stated one outright. Approval also needs the approval-blocking items **all filled with an answer**: `--plan-decision '{"status":"approved","implementationOption":"<selected option>"}'`. If any item is unfilled/reframe, do not approve and say the gate is still open. A turn-down takes the same flag with a mandatory reason: `--plan-decision '{"status":"rejected","reason":"<the user's own words>"}'` (`revision-requested` when the same plan should be reworked).
|
|
47
47
|
6. **write**: `okstra user-response write --report <reportPath> --answers '<json>' [--plan-decision '<json>'] --task-key <taskKey>` → report the returned `{sidecar:<path>}`. (When a same-named sidecar exists, the same `id` is overwritten with the new value and merged.)
|
|
@@ -189,6 +189,7 @@ Runtime/install asset changes follow this checklist:
|
|
|
189
189
|
| `team` | `src/commands/execute/team.mjs` | External lead tmux-pane worker dispatch / await / teardown |
|
|
190
190
|
| `convergence` | `src/commands/execute/convergence.mjs` | Internal admin CLI for the deterministic Phase 5.5 convergence engine (`seed`/`plan-round`/`apply-round`/`apply-critic-gaps`/`finalize`/`validate`/`example`; Python: `okstra_ctl.convergence`) |
|
|
191
191
|
| `plan-items` | `src/commands/execute/plan-items.mjs` | Internal admin CLI for deterministic plan-body item extraction and exact-match validation (`extract`/`validate`; Python: `okstra_ctl.plan_items_cli`) |
|
|
192
|
+
| `agent-activity` | `src/commands/report/agent-activity.mjs` | Thin Node shim for `okstra_ctl.agent_activity`; `append` records one run-bound activity and `project` writes the validated event projection into final-report data |
|
|
192
193
|
| `report-finalize` | `src/commands/report/finalize.mjs` | Run the whole Phase 7 post-report sequence in contractual order (Python: `okstra_ctl.report_finalize`) — the single reference point shared with the Codex lead adapter |
|
|
193
194
|
| `render-views` | `src/commands/report/render-views.mjs` | Render schema v2 data with its task-specific human template, or use the schema v1 / quick-report compatibility view |
|
|
194
195
|
| `render-final-report`, `inject-report-index` | `src/commands/report/*.mjs` | Render version-selected AI handoff Markdown from data.json; v1 index injection remains compatibility-only |
|
|
@@ -237,6 +238,7 @@ Important modules:
|
|
|
237
238
|
| Module | Role |
|
|
238
239
|
|---|---|
|
|
239
240
|
| `run.py` | `prepare_task_bundle()` single authority and CLI parser; for final-verification it adapts CLI stage input into `FinalVerificationTargetRequest`, maps the acquired target into render context, and owns `verification-target.md` snapshot/digest materialization before manifests and prompts are rendered |
|
|
241
|
+
| `agent_activity.py` | Records activity rows against run-manifest identity, imports validated command evidence from worker audit sidecars, and deterministically projects the current run's `lead-events-*.jsonl` activity rows into `agentActivity[]`. Manifests without `activityContractVersion: 1` are left unchanged. |
|
|
240
242
|
| `implementation_stage.py` | `implementation` single-stage run orchestration — read the Stage Lifecycle Snapshot → pick an available Stage Map entry → provision an isolated stage worktree → publish the selected stage as run context (extracted from `run.py`) |
|
|
241
243
|
| `stage_targets.py` | Stage readiness/verification policy SSOT — from the Stage Lifecycle Snapshot (`consumers.jsonl` ledger + carry sidecar backfill + active registry reservation) it decides which stage is runnable, which commit it branches from, and what final-verification checks. `acquire_final_verification_target()` acquires the ledger, registry, worktree, Git, and optional whole-task integration facts behind one task-key mutex and returns a typed target without render-context coupling. `order_stage_closure` topologically sorts (Kahn) the dependency closure of the wizard's multi-selected stage set to produce the unattended `chain-stages` chaining order |
|
|
242
244
|
| `stage_fix_carry.py` | fix-run carry derivation for a re-run on an `implementation` stage whose latest final-report data.json carries verifier `FAIL` verdicts — collects the previous report path, previous run HEAD, failed verifiers, carried blocking findings, and a routing recommendation, which `run.py` renders into the analysis profile through the `{{FIX_RUN_CONTEXT}}` token. A first run, or a re-run after `PASS`, yields no carry and renders the token empty |
|
|
@@ -313,7 +315,7 @@ Important modules:
|
|
|
313
315
|
| `domain/`, `application/`, `ports/` | Host-neutral values and errors, wizard/run use cases, and the interaction/session/dispatch/accounting port contracts |
|
|
314
316
|
| `registry/host_registry.py`, `registry/provider_registry.py` | Discover bundled adapters plus explicit user installs under `~/.okstra/adapters/{hosts,providers}/<id>/`; project-local adapter code is outside the discovery roots |
|
|
315
317
|
| `adapters/hosts/`, `adapters/providers/` | Six bundled host strategies and the independent provider catalogs; host manifests select a native provider without merging the two axes |
|
|
316
|
-
| `lead_events.py` |
|
|
318
|
+
| `lead_events.py` | Structured JSONL events emitted by artifact-accounted lead runtimes. Its locked append path assigns monotonic `A-NNN` identifiers to activity events in the same canonical event file. |
|
|
317
319
|
| `team_reconcile.py` | stale team-member reconciliation at run-end teardown |
|
|
318
320
|
| `worker_prompt_headers.py` | shared rendering of phase-aware worker prompt anchors (`worker_prompt_headers`): coding-preflight only for implementation and compact target identity for final-verification |
|
|
319
321
|
| `worker_prompt_body.py` | provider-neutral initial analysis body/input renderer shared by Codex and external/team dispatch paths |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at every checkpoint enumerated in the lifecycle core contract (`{{OKSTRA_LEAD_CONTRACT_PATH}}` "Progress reporting (BLOCKING)") — phase-1-intake start/complete, phase-2-prompts, phase-3-team-create, phase-4-dispatch (per worker), phase-5-collect (per worker), phase-5.5-convergence (per round), phase-6-synthesis, phase-7-persist, and final `complete`. One line per checkpoint, never batched, never replaced with prose. This is the only signal the user has during multi-minute silent windows.
|
|
9
9
|
|
|
10
|
+
When the run manifest declares `activityContractVersion: 1`, call `okstra agent-activity append` before each required activity boundary. Only after the structured append succeeds, emit the matching `PROGRESS:` line and the immediately following `ACTIVITY:` projection from the same fields. If the structured append fails, do not mark that boundary completed. Never reconstruct structured activity by parsing `ACTIVITY:` conversation text.
|
|
11
|
+
|
|
12
|
+
For a new `implementation-planning` run, the plan-body sequence is initial verification → one planner self-fix → targeted re-verification → user gate. The initial verification is round 1, the targeted re-verification is round 2, and a second automatic self-fix is a contract violation. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop.
|
|
13
|
+
|
|
10
14
|
## Current Phase Boundary
|
|
11
15
|
|
|
12
16
|
- Current lifecycle phase: `{{WORKFLOW_CURRENT_PHASE}}`
|
|
@@ -31,7 +31,7 @@ It overrides only the worker-dispatch portion of the selected host relay, not th
|
|
|
31
31
|
| `await_workers` | Run `okstra team await --project-root <root> --run-manifest <path>` through the host's asynchronous shell facility. |
|
|
32
32
|
| `redispatch_worker` | Create the core-specified fresh jobs file and dispatch it with a new `dispatchKind`; never reuse a live worker conversation. |
|
|
33
33
|
| `shutdown_workers` | Run `okstra team teardown --project-root <root> --run-manifest <path>` only after the user-approved cleanup gate. |
|
|
34
|
-
| `record_lead_event` | Append
|
|
34
|
+
| `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Emit the matching `PROGRESS:` line and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
|
|
35
35
|
| `collect_usage` | Collect artifact/CLI-log-backed usage through the existing Okstra token-usage path; never substitute another runtime's session log. |
|
|
36
36
|
|
|
37
37
|
## Pane placement is not yours to compute
|
|
@@ -83,6 +83,19 @@ User-utterance interpretation rule:
|
|
|
83
83
|
|
|
84
84
|
A single okstra run frequently spans 30–120 minutes with multi-minute silent windows while workers run; without progress signals the user cannot distinguish "still working" from "hung". Lead MUST emit a single short progress line at each checkpoint below — plain user-facing text in a separate brief message (not buried inside a tool call), one line per checkpoint, format: `PROGRESS: <phase-id> <verb-phrase>`. Emit the line raw — the literal `PROGRESS:` token must begin the line. Do NOT wrap it in inline-code backticks (`` `PROGRESS: ...` ``) or a ```` ``` ```` code fence; markdown wrapping is what the post-hoc conformance validator scrapes around, and raw emit keeps the signal unambiguous.
|
|
85
85
|
|
|
86
|
+
For an `implementation-planning` run whose run manifest declares `activityContractVersion: 1`, record every required activity boundary with `okstra agent-activity append` against the manifest-provided `leadEventsPath`. The ordering is fixed: the structured append succeeds first, the matching `PROGRESS:` line is emitted second, and the immediately following `ACTIVITY:` line projects the same structured fields into the conversation language. Do not reconstruct structured activity from conversation text. If the append fails, do not present that activity boundary as completed.
|
|
87
|
+
|
|
88
|
+
The live projection follows this shape:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
PROGRESS: phase-4-dispatch worker=codex-worker model=gpt-5.6-sol
|
|
92
|
+
ACTIVITY: id=A-001 agent=codex-worker summary="Verify Stage Map paths and commands" items=P-Step-001,P-Step-002 result=runs/.../codex-worker-....md outcome=pending
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use the exact CLI projection: `id`, `agent`, quoted `summary`, comma-joined `items` (`<none>` when empty), `result` (`<none>` when empty), and `outcome`. Only prose inside `summary` is localized to the conversation language. Required kinds are `worker-dispatched`, `worker-completed`, `verification-round-completed`, `self-fix-applied`, `user-decision-required`, and `user-decision-evaluated`.
|
|
96
|
+
|
|
97
|
+
**Enforcement:** `tests/contract/test_host_orchestration_rules.py` keeps this instruction on every lead path. `validators/validate_session_conformance.py` `_check_activity_contract` checks the structured event log and does not treat an `ACTIVITY:` conversation line as evidence.
|
|
98
|
+
|
|
86
99
|
Required checkpoints:
|
|
87
100
|
|
|
88
101
|
- `PROGRESS: phase-1-intake reading task bundle` — at the start of Phase 1, before issuing parallel Read calls.
|
|
@@ -106,7 +119,7 @@ Do NOT replace them with prose ("Now I'm starting Phase 2..."), do NOT skip a ch
|
|
|
106
119
|
|
|
107
120
|
`okstra-run` surfaces these lines to the user directly; other launch paths persist them in the selected adapter's declared conformance evidence/event source for post-hoc retrieval.
|
|
108
121
|
|
|
109
|
-
**Enforcement:** the Phase 7 validator (`validators/validate-run.py` → `validate_session_conformance.py`) reads the selected adapter's declared conformance evidence/event source within the run window and fails the run as `contract-violated` when a required checkpoint is missing — including the per-worker `phase-4-dispatch` / `phase-5-collect` lines (which must name each worker's role) and the `phase-batch-cleanup` lines that MUST precede the first `phase-5.5-convergence` round and the `phase-6-synthesis` report-writer dispatch. When the plan-body state file records two or more rounds, `_check_plan_verify_cleanup_checkpoints` additionally requires a `phase-5.5.9-plan-verify` line per round and a `phase-batch-cleanup` between consecutive rounds
|
|
122
|
+
**Enforcement:** the Phase 7 validator (`validators/validate-run.py` → `validate_session_conformance.py`) reads the selected adapter's declared conformance evidence/event source within the run window and fails the run as `contract-violated` when a required checkpoint is missing — including the per-worker `phase-4-dispatch` / `phase-5-collect` lines (which must name each worker's role) and the `phase-batch-cleanup` lines that MUST precede the first `phase-5.5-convergence` round and the `phase-6-synthesis` report-writer dispatch. When the plan-body state file records two or more rounds, `_check_plan_verify_cleanup_checkpoints` additionally requires a `phase-5.5.9-plan-verify` line per round and a `phase-batch-cleanup` between consecutive rounds. For activity-contract-v1 planning, `_check_activity_contract` validates the structured worker pairs, verification and self-fix counts, user-decision references, and `A-NNN` ordering. `phase-7-teardown` and `complete` fire after validation and are not checked.
|
|
110
123
|
|
|
111
124
|
## User confirmation before an approval blocker (BLOCKING)
|
|
112
125
|
|
|
@@ -118,9 +131,22 @@ The sequence is fixed:
|
|
|
118
131
|
|
|
119
132
|
1. Emit `PROGRESS: user-confirm <C-NNN> <the question, one line>` with the id the row would carry.
|
|
120
133
|
2. Ask in plain user-facing text: what is undecided, the options with their consequences, and which one you recommend. One question at a time.
|
|
121
|
-
3. On an answer — record
|
|
134
|
+
3. On an answer — record the raw text in the row's `userInput`, set `status: answered` and `userConfirmation: asked-and-answered`, and apply the selected disposition in this run.
|
|
122
135
|
4. Only when asking fails does the row stay open: `asked-awaiting` when the user has not answered, `deferred-no-interactive-session` when this run has no user to ask.
|
|
123
136
|
|
|
137
|
+
For activity-contract-v1 `implementation-planning`, every approval row carries `approvalContext`. Classify a user-owned selection as `user-decision`, a surviving non-correctness majority disagreement as `noncritical-dissent`, and a cited path/symbol mismatch, `P-Req-*` coverage mismatch, or independent Requirement Coverage blocker as `correctness-critical`. `select` is limited to `user-decision`, `accept-risk` is limited to `noncritical-dissent`, and `request-revision` / `reject` are available to all three classifications. `correctness-critical` never offers or records `accept-risk`. **Enforced:** `validators/validate-run.py` `_validate_approval_context` recomputes the classification, disposition allowlist, activity references, and resolved-state requirements.
|
|
138
|
+
|
|
139
|
+
The approval state transitions are fixed:
|
|
140
|
+
|
|
141
|
+
- `open → answered` when the raw user response is recorded
|
|
142
|
+
- `answered → resolved` only after the selected disposition is applied and its checks pass
|
|
143
|
+
- `answered → open` when application or checking fails
|
|
144
|
+
- `open → obsolete` only when a plan change removes the question
|
|
145
|
+
|
|
146
|
+
`open` and `answered` continue to block approval; only `resolved` and `obsolete` are non-blocking. `user-decision` resolves after the choice is applied and structure / extraction / Requirement Coverage checks pass. `noncritical-dissent` resolves only after an explicit `accept-risk` with non-empty user text and activity-backed checks. `correctness-critical` resolves only after the correction's targeted re-verification records `AGREE` or an acceptable `SUPPLEMENT` for every linked item and no independent coverage blocker remains. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop.
|
|
147
|
+
|
|
148
|
+
When a terminal row preserves a pre-correction dissent classification, keep the superseded votes in `state/plan-body-verification-implementation-planning-<seq>.json`; the validator recomputes the historical class from those votes and never trusts `approvalContext.classification` alone. Each cited `user-decision-required` and `user-decision-evaluated` activity records the row's exact `C-NNN` in `evidenceRefs` and covers every `approvalContext.planItemIds` value; an evaluated activity also records check evidence beyond the `C-NNN` itself. A corrected coverage-only blocker keeps its `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`, and the state-sidecar plan item without a historical blocking dissent that participated in the `coverage-gap` round keeps the same `C-NNN` in `clarificationId`; a run-wide `coverage-gap` without that item-level link is not evidence for the row. An `obsolete` row is invalid while its disagreement or coverage blocker remains active in the current plan. **Enforced:** `validators/validate-run.py` `_read_approval_history`, `_activity_matches_approval_context`, `_historical_coverage_clarification_ids`, and `_validate_approval_context`.
|
|
149
|
+
|
|
124
150
|
**Predicting the blocker is not the same as raising it.** A lead that says "this will likely become an approval blocker; I will ask at that point" has already reached the moment — ask then, in that message. One run announced exactly that, never asked, wrote the row anyway, and then spent its entire self-fix budget on a gate no round could clear, because the user had already answered the question before the run started.
|
|
125
151
|
|
|
126
152
|
**`lead-directed` blockers cannot be deferred.** When the item is the lead's own judgment rather than a worker's finding, and this run has nobody to ask, the row is not the outlet — record a Working Assumption in `## 5. Missing Information and Risks` naming the assumption the plan proceeds under, exactly as a surviving planner-fixable item does, and let the plan proceed. Blocking a plan on the lead's own judgment in a run where that judgment cannot be put to the user only moves the work to a re-run.
|
|
@@ -361,6 +387,8 @@ Distinct from Phase 5.5 finding convergence:
|
|
|
361
387
|
|
|
362
388
|
Lead's responsibilities in this sub-step (in order):
|
|
363
389
|
|
|
390
|
+
For a new `implementation-planning` run, the fixed order is initial verification → one planner self-fix → targeted re-verification → user gate. The initial verification is round 1 and the targeted re-verification is round 2. A second automatic self-fix is a contract violation.
|
|
391
|
+
|
|
364
392
|
1. Build the queue with `okstra plan-items extract --data <data.json> --output <state>/plan-items-....json`, place the persisted `items[]` verbatim in every verifier prompt, then run `okstra plan-items validate --data <data.json> --items <state>/plan-items-....json`. The lead MUST NOT summarise, select, omit, reorder, or renumber the queue. Each prompt uses the compact `subject` plus the lossless `payload`, and asks every item:
|
|
365
393
|
|
|
366
394
|
```text
|
|
@@ -371,7 +399,7 @@ Lead's responsibilities in this sub-step (in order):
|
|
|
371
399
|
An `AGREE` response records the considered counterexample and exclusion reason in its note; unverified external material is `verification-error`, not `DISAGREE`.
|
|
372
400
|
2. Dispatch a single plan-body reverify round to every analyser worker in the roster (`claude`, `codex`, and `antigravity` when opted in). `Report writer worker` is NOT a participant in this round.
|
|
373
401
|
3. Aggregate verdicts and resolve the gate result to one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`.
|
|
374
|
-
4. Write `runs/<task-type>/state/plan-body-verification.json` (schema in the plan-body-verification contract), appending
|
|
402
|
+
4. Write `runs/<task-type>/state/plan-body-verification.json` (schema in the plan-body-verification contract), appending round 1 and, if the one automatic rewrite ran, round 2 to `roundHistory[]`; data.json keeps only the final verdicts.
|
|
375
403
|
5. Populate `implementationPlanning.planBodyVerification` in data.json with round count, gate result, per-item verdicts, and dissent log. The AI handoff task-deliverable block carries this structure without a second prose rendering.
|
|
376
404
|
6. For every `majority-disagree` plan item, append one `clarificationItems[]` row with `blocks=approval` and the 1:1 ID match in the verdict classification (`majority-disagree → C-<N>`). Do not create a parallel open-questions structure.
|
|
377
405
|
7. Publish the YAML frontmatter `approved:` field as `false`. There is no in-body `- [ ] Approved` marker line — approval lives only in the frontmatter (see [plan-body-verification](./plan-body-verification.md) §"Round protocol" step 9). The user may flip it to `true` only when the gate is `passed` or `passed-with-dissent`. **Enforced:** `validators/validate-run.py` `validate_phase_boundary` fails a report shipping `approved: true` under `blocked-by-disagreement` / `aborted-non-result`, and run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan`) fail-closes the same case. Manually flipping a blocked gate to passing is a contract violation.
|
|
@@ -384,14 +412,10 @@ The detailed persistence checklist and the BLOCKING token-usage collector invoca
|
|
|
384
412
|
|
|
385
413
|
Order of operations:
|
|
386
414
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
4. Update run manifest.
|
|
392
|
-
5. Update `task-manifest.json`, including lifecycle fields (work category, phase states, next recommended phase, approval markers, safe-resume checkpoint).
|
|
393
|
-
6. Update `task-index.md`.
|
|
394
|
-
7. Write final status file if expected.
|
|
415
|
+
1. Run `okstra agent-activity project --project-root <root> --run-manifest <path> --data <data.json>`. This deterministically projects the canonical lead-events activity rows before any prose inspection.
|
|
416
|
+
2. Run `okstra report-translate check-source <data.json>` even when `meta.reportLanguage` is `en`.
|
|
417
|
+
3. When `meta.reportLanguage` is not `en`, dispatch the translator worker. The worker builds its work list with `okstra report-translate extract`, writes `final-report-<task-type>-<seq>.i18n.<lang>.json`, and gates it with `okstra report-translate check`.
|
|
418
|
+
4. Run `okstra report-finalize ...`. This command owns token substitution, view rendering, follow-up persistence, and validation in their contractual order.
|
|
395
419
|
|
|
396
420
|
Keep the assigned worker prompt history paths stable in `team-state`, `run-manifest`, and `task-manifest`. Do not rewrite prompt artifacts to `/tmp` or omit prompt metadata for attempted workers.
|
|
397
421
|
|
|
@@ -443,4 +467,4 @@ After persistence, reply briefly in the resolved Report Language with: completio
|
|
|
443
467
|
| Waiting silently after `dispatch_worker` returns without a completed worker artifact | A dispatch acknowledgement is not completion — call `await_workers` and enforce the selected adapter's liveness policy |
|
|
444
468
|
| Re-sending a finding absent from the persisted round plan | Dispatch exactly the engine-returned `findingIds`; see [convergence](./convergence.md) "Re-verification Dispatch" |
|
|
445
469
|
| Aggregating a `timeout`/`error` reverify dispatch as `DISAGREE` | Put the terminal outcome in round results; `apply-round` records `verification-error`. See [convergence](./convergence.md) "Worker failure handling in reverify" |
|
|
446
|
-
|
|
|
470
|
+
| Bypassing `report-finalize` and running its Phase 7 steps manually | Run `okstra report-finalize ...`; it owns token substitution and the remaining persistence order. |
|
|
@@ -44,7 +44,7 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
|
|
|
44
44
|
|---------|---------|-------------|
|
|
45
45
|
| `enabled` | `true` | If `false`, the round is skipped and the approval gate is not blocked by this round (legacy behaviour). |
|
|
46
46
|
| `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
|
|
47
|
-
| `selfFixMaxRounds` | `
|
|
47
|
+
| `selfFixMaxRounds` | `1` | One report-writer rewrite at most. The initial verification is round 1; targeted re-verification is round 2 after that rewrite. |
|
|
48
48
|
| `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. |
|
|
49
49
|
|
|
50
50
|
Default values are emitted into the manifest by `scripts/okstra_ctl/render.py` (`_build_convergence_block`). The ctx knob `OKSTRA_PLAN_VERIFICATION=false` flips `planBodyVerification.enabled` to false.
|
|
@@ -237,35 +237,44 @@ round before any host or provider process starts.
|
|
|
237
237
|
|
|
238
238
|
**How the corrective round is recorded.** The first prompt was dispatched, so it is immutable — `--replace-undispatched` refuses it, correctly. Materialize the correction under a NEW `--invocation-id` and a new prompt path. Before linking its result, retire the first attempt's link: `okstra agent-prompt reject-result --run-manifest <path> --dispatch-id <first dispatch id> --superseded-by <corrective dispatch id> --reason "<what was wrong with the returned result>"`. Without that step the corrective `link-result` fails with `agent result is already linked to another dispatch`, which is how a worker that ran for twenty minutes and wrote a good result ends up unrecordable. Nothing is deleted: the rejected link stays in `agentResultLinks` carrying `supersededBy` and `rejectionReason`, so the ledger shows both attempts and why the second exists.
|
|
239
239
|
|
|
240
|
-
Then lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below), **appending this round** — one new `roundHistory[]` entry plus this round's votes on each verified item's `planItems[].rounds[]`. The file accumulates across rounds; it is never truncated to the latest one. Lead then populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
241
|
-
7. **Self-fix loop (
|
|
240
|
+
Then lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below), **appending this round** — one new `roundHistory[]` entry plus this round's votes on each verified item's `planItems[].rounds[]`. The file accumulates across rounds; it is never truncated to the latest one. After `okstra plan-verify` exits 0, lead sets that new round's `completedAt` to the current ISO 8601 UTC time exactly once; a prior round's `completedAt` is immutable. Lead then populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
241
|
+
7. **Self-fix loop (one rewrite, targeting planner-fixable defects).** After round 1, lead may run one report-writer rewrite when at least one `majority-disagree` item has a majority of its `DISAGREE` verdicts at `fixability == planner-fixable`. The targeted re-verification after that rewrite is round 2. After round 2, stop automatic self-fix regardless of outcome. Classify every remaining item as `user-decision`, `noncritical-dissent`, or `correctness-critical`. A second automatic self-fix is a contract violation. The fixed order is initial verification → one planner self-fix → targeted re-verification → user gate.
|
|
242
242
|
- **Group the targets by cause before instructing (BLOCKING).** Blocked items are usually several derivatives of one defect — one constant declared twice, one responsibility given two owners — and the coverage rows that cite them fail as a consequence, not independently. Lead MUST partition this round's targets into cause groups and instruct each group as **"remove this cause"**, naming the derivatives it accounts for. **Handing report-writer a bare item list is forbidden**: patched one at a time, each correction leaves the sibling sections still asserting the old value, so the next round re-finds the same family and the budget drains without converging. Record the partition in `planBodyVerification.selfFixGroups[]` (`round`, `causeSummary`, `itemIds`). One group per item is a legitimate outcome only when the items genuinely share no cause — recorded that way, it is a visible diagnosis rather than a skipped one. **Enforced:** `validators/validate-run.py` `_validate_self_fix_grouping` requires the partition, ties `selfFixRoundsApplied` to the highest recorded round, and fails any corrected item that belongs to no group.
|
|
243
243
|
- lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration; procedure in [report-writer](./report-writer.md) §"Self-fix rewrite").
|
|
244
244
|
- missing or weak `P-Prep-*` contracts are repaired by adding kind-specific inline detail or an AI-prepared PREP item with a concrete proposal. Facts that require user or external authority remain `blocked` and keep their request material; never invent those facts during self-fix.
|
|
245
245
|
- **Drop plan items whose element the round deleted.** A self-fix rewrite may remove a plan element (a validation check, a rollback row). `P-*` ids are positional, so a deletion shifts every later row and silently re-points surviving verdicts at their neighbours — and a verdict recorded against a removed element keeps blocking a gate while being unfindable in the plan, so reading the plan never reveals the cause. After each round, re-extract plan items with `okstra plan-items extract` and re-verify any item whose `subject` no longer matches; never carry the old vote forward across a shift. **Enforced:** `validators/validate-run.py` `_validate_verdicts_match_current_subjects` (re-pointing) and `_validate_plan_item_extraction_completeness` (dangling ids).
|
|
246
246
|
- **Classify each cause group before instructing it (BLOCKING).** A group is either an *authoring* defect — the plan says something wrong, incomplete, or self-contradictory, which self-fix owns — or a *citation* defect, where the plan points at an analysis artifact incorrectly. Only the first is self-fix work. For the second the finding already exists and already went through convergence, so the fix is to re-cite the converged artifact; instructing report-writer to re-derive the fact means the author reads the source material and produces a **finding that never went through convergence**, which the plan then carries as if it had. That is the role boundary the lead contract draws ("keep analysis, execution, verification, and report authoring responsibilities distinct; return defects to the role that owns them"), and report-writer is authoring-only by its own contract. `P-Req-*` items with breakage kind `f` are where this goes wrong most often: the question is usually whether a coverage row points correctly at something already measured, not whether the measurement is right. State the classification in the group's instruction so the author knows which of the two it is being asked to do.
|
|
247
|
-
- **A verdict older than the last self-fix is not a verdict (BLOCKING).**
|
|
247
|
+
- **A verdict older than the last self-fix is not a verdict (BLOCKING).** A verdict cast in round 1 judged the text before the only automatic rewrite. Once that rewrite runs, the judgement is about a plan that no longer exists. `--round <N>` on `apply-verdicts` stamps each row, and `validators/validate-run.py` `_validate_verdict_rounds_outlive_self_fix` fails any non-carried item whose verdict round is at or before `selfFixRoundsApplied`. Before declaring the gate, every item still holding a pre-self-fix verdict MUST be re-verified in round 2.
|
|
248
248
|
- lead re-runs plan-body verification (focused on the corrected items + adjacent items the rewrite touched, plus any `needs-reverify` items whose peer failed to vote last round). After re-verification, overwrite `planItems[].verdicts` with the new verdicts. **The round's verdicts MUST be transcribed into `planBodyVerification.planItems[].verdicts` in the final report's data.json before the gate is declared** — the gate is re-derived from that table, so declaring a gate over an empty one leaves it unauditable. **Enforced:** `_validate_round_recorded_verdicts`. Transcribe with `okstra plan-items collect-verdicts --result <worker>=<path> … --items <plan-items.json> --output <verdicts.json>` then `okstra plan-items apply-verdicts --data <data.json> --verdicts <verdicts.json> --round <N>`, never with a per-round script: the CLI reads the response shape this section fixes and **fails** on an assigned item the worker left unanswered, on a verdict for an item outside the queue, and on a `DISAGREE` with no breakage kind. A hand-written regex reports none of those — it drops them, and the round is then scored on a table that silently does not match the queue.
|
|
249
249
|
- for an item whose `majority-disagree` was resolved by self-fix, record `self-fixed in round <N>: <what was fixed>` in `planItems[].selfFixNote`. A resolved item does not create a clarification.
|
|
250
250
|
- **Each round is a worker batch.** Before dispatching round N ≥ 2, reclaim the previous round's completed verifiers exactly as at any other batch boundary ([okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle") and emit `PROGRESS: phase-batch-cleanup panes=<n>`, then announce the round with `PROGRESS: phase-5.5.9-plan-verify round=<N> items=<count>`. Saying a round will "reuse" the previous verifiers and then dispatching under fresh names leaves every prior round holding its panes — five rounds of that is what exhausts the pane budget and blocks the next dispatch. **Enforced:** `validators/validate_session_conformance.py` `_check_plan_verify_cleanup_checkpoints` requires both lines once the state file records two or more rounds.
|
|
251
|
-
- **Round completion.** A round is complete only after the renderer has run on the corrected data.json, lead has appended the round to the state file per step 6, lead has reconciled instructed groups against applied corrections — every `itemIds` entry either carries a `selfFixNote` or is still recorded as broken —
|
|
252
|
-
- **Loop termination.** Lead — not the report-writer worker — records the round count in `planBodyVerification.selfFixRoundsApplied` at each round's end, and why the loop stopped in `planBodyVerification.selfFixStopReason`. The count must equal the highest `round` in `selfFixGroups[]`, so it is derivable from recorded work rather than self-reported:
|
|
251
|
+
- **Round completion.** A round is complete only after the renderer has run on the corrected data.json, lead has appended the round to the state file per step 6, lead has reconciled instructed groups against applied corrections — every `itemIds` entry either carries a `selfFixNote` or is still recorded as broken — **`okstra plan-verify --report <report>` exits 0** (step 5), and lead has then set that round's immutable `completedAt`. A round left with a non-zero exit carries its defect into the next round's inputs, which is how a mis-scored gate survives a whole self-fix budget. A round that was instructed but never rendered has not happened, and counting it inflates the budget that gates promotion. The state-file append is not optional bookkeeping: the next re-verification overwrites data.json's `planItems[].verdicts`, so a round that never reached `roundHistory[]` leaves no record anywhere of what it blocked on — which is the whole reason this file exists. **Enforced:** `validators/validate-run.py` `_validate_plan_body_state_rounds` requires one `roundHistory[]` entry per round `1..roundCount`, each carrying its own `gateResult` and cited by at least one item's `rounds[]`, and requires the file's `selfFixRoundsApplied` to match the report's. For a resolved correctness-critical user response, `_validate_target_round_causality` additionally requires the cited round's `completedAt` to be after every linked canonical `user-decision-required` event and no later than every linked canonical `user-decision-evaluated` event.
|
|
252
|
+
- **Loop termination.** Lead — not the report-writer worker — records the round count in `planBodyVerification.selfFixRoundsApplied` at each round's end, and why the loop stopped in `planBodyVerification.selfFixStopReason`. The count must equal the highest `round` in `selfFixGroups[]`, so it is derivable from recorded work rather than self-reported. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop:
|
|
253
253
|
- `all-resolved` — no planner-fixable `majority-disagree` item remains. Exit.
|
|
254
254
|
- `no-progress` — the round resolved **zero** planner-fixable items relative to the previous round. Exit even with budget left: the same rewrite would repeat. Newly *introduced* defects count against progress, so a rewrite that trades one defect for another stops the loop rather than churning.
|
|
255
255
|
- `max-rounds-reached` — `selfFixRoundsApplied == selfFixMaxRounds`. Exit.
|
|
256
|
-
- `cause-group-recurrence` —
|
|
256
|
+
- `cause-group-recurrence` — legacy read-only value for reports produced before activity contract v1. A new activity-contract-v1 run MUST NOT emit it because there is no second automatic self-fix round in which a cause group can recur. **Enforced:** `validators/validate-run.py` `_validate_activity_contract_plan_limits`.
|
|
257
257
|
- `not-attempted` — the loop never ran because no item qualified.
|
|
258
258
|
The `no-progress` and `max-rounds-reached` exits are what make the loop terminate; `selfFixMaxRounds` alone is the backstop.
|
|
259
|
-
- a `majority-disagree` item with a majority of `needs-user-input` is NOT a self-fix target — it goes straight to the next step
|
|
259
|
+
- a `majority-disagree` item with a majority of its deciding `DISAGREE` votes at `needs-user-input` is NOT a self-fix target — after correctness-critical precedence, it goes straight to the next step as `user-decision` rather than generic `noncritical-dissent`. **Enforced:** `validators/validate-run.py` `_expected_approval_classification`.
|
|
260
260
|
8. For every `majority-disagree` item **that remains after the self-fix loop** (items not resolved by self-fix, or with a `needs-user-input` majority from the start), lead adds a row to `## 1. Clarification Items` with:
|
|
261
261
|
- new `C-<N>` ID (numbering continues from any existing rows)
|
|
262
262
|
- `Statement` summarising the disagreement and the worker breakage `<kind>`
|
|
263
263
|
- `Kind` chosen per the standard policy (usually `decision` for option-level conflicts, `data-point` for path/symbol mismatches)
|
|
264
264
|
- `Blocks=approval`
|
|
265
265
|
- the item's `planItems[].clarificationId` set to that `C-<N>` (1:1 link). `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item's `clarificationId` is missing, dangling, or points at a non-`approval` row.
|
|
266
|
-
-
|
|
267
|
-
-
|
|
266
|
+
- set `approvalContext.classification` to `user-decision` for a majority `needs-user-input` item, `correctness-critical` for `DISAGREE(a)`, `DISAGREE(f)` on `P-Req-*`, or an independent Requirement Coverage blocker, and `noncritical-dissent` for another surviving majority disagreement.
|
|
267
|
+
- populate `approvalContext.planItemIds`, `activityIds`, `unblockCondition`, and `recommendedDisposition`. Every option carries a `disposition`: `select` only for `user-decision`, `accept-risk` only for `noncritical-dissent`, and `request-revision` / `reject` for any classification. `correctness-critical` never offers or records `accept-risk`. **Enforced:** `validators/validate-run.py` `_validate_approval_context`.
|
|
268
|
+
- **Self-fix exhaustion is not risk acceptance.** A `noncritical-dissent` item remains blocking until the user explicitly selects `accept-risk`. Record the user's non-empty original text and the `user-decision-required` / `user-decision-evaluated` activity references in `approvalContext.resolution`; only then does `validators/validate-run.py` `_resolved_noncritical_dissent_ids` let `_is_dissent_downgraded` fold it into `passed-with-dissent`.
|
|
269
|
+
- **Correctness-critical defects cannot be waived.** After the user-directed correction, targeted re-verification of every linked item MUST record only `AGREE` or an acceptable `SUPPLEMENT`, and any independent Requirement Coverage blocker MUST be removed before the row becomes `resolved`. A `DISAGREE` or `verification-error` returns it to `open`. **Enforced:** `validators/validate-run.py` `_validate_correctness_resolution`.
|
|
268
270
|
- When a correctness-critical `planner-fixable` item is promoted, its `Statement` MUST state "planner self-fix attempted but unresolved" and name the stop reason. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails when a planner-fixable majority item is promoted while the budget is not exhausted — it requires `selfFixRoundsApplied >= 1` **and** `selfFixStopReason` in `{no-progress, max-rounds-reached}`, so neither `all-resolved` nor `not-attempted` can excuse a promotion.
|
|
271
|
+
- Approval state transitions are fixed:
|
|
272
|
+
- `open → answered` when the raw user response is recorded
|
|
273
|
+
- `answered → resolved` only after the selected disposition is applied and its checks pass
|
|
274
|
+
- `answered → open` when application or checking fails
|
|
275
|
+
- `open → obsolete` only when a plan change removes the question
|
|
276
|
+
`open` and `answered` continue to block approval; only `resolved` and `obsolete` are non-blocking. A user-directed correction does not consume the automatic self-fix limit, and a failed check does not restart the automatic loop.
|
|
277
|
+
- A terminal row may preserve its original dissent classification only from audited history. Keep superseded votes in `state/plan-body-verification-implementation-planning-<seq>.json`; `validators/validate-run.py` `_historical_plan_item_evidence` recomputes criticality from the recorded `DISAGREE(a|f)` tokens and does not trust the row's classification alone. Every referenced `user-decision-required` / `user-decision-evaluated` activity must cite exactly that row's `C-NNN` and exactly the linked `approvalContext.planItemIds` set. The evaluated activity occurs after every required activity, has `outcome: resolved`, records at least one command whose every `exitCode` is `0`, points `resultPath` at the matching plan-body state artifact, and cites exactly one `plan-body-verification:round-N` evidence token. Round `N` is later than the recorded blocking round; its state votes are all `AGREE` or `SUPPLEMENT`, and they exactly match the final-report verdicts. Its immutable `completedAt` is later than every referenced required activity's canonical event timestamp and no later than every referenced evaluated activity's canonical event timestamp, so an older successful round cannot be relabelled as the response check. This user-response round is recorded in `roundHistory[]` but does not increment `selfFixRoundsApplied` or create another automatic `verification-round-completed` activity. Only the exact round token in `resolution.checkRefs` of a resolved `correctness-critical` row receives that exclusion; the referenced resolved `user-decision-evaluated` activity must match the row's exact `C-NNN` and plan-item set. **Enforced:** `validators/validate-run.py` `_validate_approval_activity_refs`, `_validate_correctness_resolution`, and `_validate_target_round_causality`, plus `validators/validate_session_conformance.py` `_resolved_correctness_reverification_rounds` and `_check_activity_round_counts`. When an independent coverage-only blocker is corrected, keep the `C-NNN` in the now non-blocking Requirement Coverage row's `decisionRefs` and in the matching state-sidecar plan item's `clarificationId`; that item must have no historical blocking dissent and must participate in a round whose `gateBlockedBy` contains `coverage-gap`. A run-wide `coverage-gap` without this item-level `C-NNN` link cannot classify another row. `obsolete` is valid only after current evidence shows that the question or blocker disappeared, or the linked item is historical and removed; a current linked item remains active even when audited history preserves an older classification.
|
|
269
278
|
9. Approval lives in the report's YAML frontmatter `approved:` field — there is no in-body marker line. The user may flip it to `true` only when the Gate result is `passed` or `passed-with-dissent`. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan`) fail-closes an `approved: true` plan whose data.json carries a blocking `gateResult` or an open/answered `Blocks: approval` clarification row, and `validators/validate-run.py` `_validate_plan_body_gate_recompute` rejects a declared `gateResult` healthier than the recorded votes.
|
|
270
279
|
|
|
271
280
|
## `plan-body-verification-<task-type>-<seq>.json` schema
|
|
@@ -329,6 +338,7 @@ The per-round structures mirror the finding-convergence state artifact ([converg
|
|
|
329
338
|
"roundHistory": [
|
|
330
339
|
{
|
|
331
340
|
"round": 1,
|
|
341
|
+
"completedAt": "2026-08-15T01:20:00Z",
|
|
332
342
|
"gateResult": "blocked-by-disagreement",
|
|
333
343
|
"gateBlockedBy": ["majority-disagree"],
|
|
334
344
|
"dispatches": [
|
|
@@ -337,6 +347,7 @@ The per-round structures mirror the finding-convergence state artifact ([converg
|
|
|
337
347
|
},
|
|
338
348
|
{
|
|
339
349
|
"round": 2,
|
|
350
|
+
"completedAt": "2026-08-15T01:35:00Z",
|
|
340
351
|
"gateResult": "passed-with-dissent",
|
|
341
352
|
"gateBlockedBy": [],
|
|
342
353
|
"dispatches": [
|
|
@@ -349,7 +360,7 @@ The per-round structures mirror the finding-convergence state artifact ([converg
|
|
|
349
360
|
|
|
350
361
|
> Abbreviated example: a one-round run has a single `roundHistory[]` entry and a single `rounds[]` entry per item. `P-Opt-1` above is not re-verified in round 2 because the round is focused on the corrected items and the ones the rewrite touched (step 7) — an item may legitimately carry fewer `rounds[]` entries than `roundHistory[]` has rounds, but every round in `roundHistory[]` must appear on at least one item.
|
|
351
362
|
|
|
352
|
-
`roundHistory[].gateResult` / `gateBlockedBy` are that round's own gate resolution (§"Round protocol" step 5), not the run's final one — the final value lives in data.json. `dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run`). A wrapper-recorded `cli-failure` is a run-error-log event, not a terminal status — record that dispatch's `terminalStatus` as `error`.
|
|
363
|
+
`roundHistory[].gateResult` / `gateBlockedBy` are that round's own gate resolution (§"Round protocol" step 5), not the run's final one — the final value lives in data.json. `roundHistory[].completedAt` is the immutable ISO 8601 UTC timestamp recorded once after the round passes `okstra plan-verify`; it is not copied from a later activity and is never revised. `dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run`). A wrapper-recorded `cli-failure` is a run-error-log event, not a terminal status — record that dispatch's `terminalStatus` as `error`.
|
|
353
364
|
|
|
354
365
|
`planItems[].rounds[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | needs-reverify | contested`. `needs-reverify` is the peer-error shape from §"Round protocol" step 4 (a single-vote-blocking kind with fewer than 2 participating non-error votes) — it survives into the state file when the round budget runs out before the re-dispatch resolves it, and `_recompute_plan_body_gate` folds it into `passed-with-dissent`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
|
|
355
366
|
|
|
@@ -85,11 +85,11 @@ For an implementation-planning run, the Report writer worker owns the Phase 6 de
|
|
|
85
85
|
|
|
86
86
|
### Before `report-finalize`: the translation sidecar (BLOCKING order)
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
The finalization renderer overlays the translation sidecar, so a non-English run must produce that sidecar before finalization. Use this fixed order:
|
|
89
89
|
|
|
90
|
-
1. **
|
|
91
|
-
2. **Only when
|
|
92
|
-
3.
|
|
90
|
+
1. **For a non-English report only**, run `okstra report-finalize ... --only project-activity --only check-source`. The shared finalizer projects canonical activity before checking the English source. A historical manifest without `activityContractVersion: 1` leaves data.json unchanged.
|
|
91
|
+
2. **Only when that check passes**, dispatch the translator worker, which writes `final-report-<task-type>-<seq>.i18n.<lang>.json`.
|
|
92
|
+
3. Run the full `report-finalize` command below. English reports start here; the finalizer repeats the idempotent projection and source check before every downstream step.
|
|
93
93
|
|
|
94
94
|
For step 2, write translator-only task instructions and run `okstra
|
|
95
95
|
agent-prompt materialize` with `--audience translator`, `--assignment-ref
|
|
@@ -116,17 +116,18 @@ okstra report-finalize \
|
|
|
116
116
|
--report <runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Do NOT run the
|
|
119
|
+
Do NOT run the six steps below by hand. Hand-running them is the recurring root cause of reports shipping with stale activity, `--` token cells, a missing html sibling, Section 3 missing follow-up entries, or Section 4 rows never spawning — the order is load-bearing and a skipped step surfaces only later, as a validator `contract-violated`. Every step is idempotent, so after fixing a reported failure just re-run the same command.
|
|
120
120
|
|
|
121
121
|
The steps it executes, in this contractual order, and the contract each one carries:
|
|
122
122
|
|
|
123
|
-
1. **`
|
|
124
|
-
2. **`
|
|
123
|
+
1. **`project-activity` — project canonical activity.** Replaces only `agentActivity[]` from this run's canonical events before translation source extraction. A legacy manifest without activity contract v1 is a byte-preserving no-op. Conformance compares IDs, order, and every core field against the canonical events.
|
|
124
|
+
2. **`check-source` — verify the data.json is English.** The same gate as the pre-translator check above, run again here because everything after it derives from the data.json: rendering a Korean SSOT into English chrome, spawning follow-ups from it, and validating it all succeed on a record the next phase cannot read. A failure here means the report-writer authored in the reader's language; re-dispatch it with the English rule rather than editing the data.json by hand.
|
|
125
|
+
3. **`token-usage` — collect usage.** Aggregates `leadUsage` / `workers[].usage` / `usageSummary` into team-state, populates `tokenUsage` and the execution-status usage fields in data.json, and re-invokes the renderer so the markdown carries real numbers.
|
|
125
126
|
|
|
126
127
|
The data.json paths populated: `tokenUsage.lead.{totalTokens,billableTokens,costUsd}`, the `worker` / `grand` rows, `tokenUsage.cli.costUsd`, and each `executionStatus[].{totalTokens,billableTokens,costUsd,durationMs,cliTotalTokens,cliCostUsd}` for rows whose role matches a team-state worker. The data.json MUST already exist (Phase 6 output).
|
|
127
128
|
|
|
128
129
|
For implementation-planning, this Phase 7 canonical render calls `materialize_design_prep_requests()` after token substitution and creates deterministic request files only for `provisional` / `blocked` items. Later answers are append-only user-input sidecars; request generation and user input never rewrite the assessment fields, so the source report remains immutable as the design-input snapshot after this render. `validators/validate-run.py` `_validate_design_prep_requests` enforces request existence, canonical path, content, and assessment fingerprint.
|
|
129
|
-
|
|
130
|
+
4. **`render-views` — render the human report artifact.** Runs against the substituted v2 data.json and its Markdown sibling.
|
|
130
131
|
|
|
131
132
|
Output (idempotent — re-running overwrites):
|
|
132
133
|
- `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, always generated for schema v2 from the dedicated template registered for that task type. Clarification rows with `Status` ∈ {`open`, `answered`} embed response controls and export a `user-response-<task-type>-<seq>.md` sidecar. The original data and Markdown artifacts are never mutated by user input.
|
|
@@ -134,7 +135,7 @@ The steps it executes, in this contractual order, and the contract each one carr
|
|
|
134
135
|
- Schema-v1 and quick compatibility reports retain the legacy conditional HTML path; this does not change the schema-v2 always-generated contract.
|
|
135
136
|
|
|
136
137
|
It runs after usage collection so token placeholders are substituted in any rendered html, and before routing persistence so the html artifact, when generated, exists for the validator step that checks it. It also overlays the translation sidecar, which is why a non-English run must dispatch the translator before this command — see the ordering rule above.
|
|
137
|
-
|
|
138
|
+
5. **`spawn-followups` — routing and follow-up persistence.** Turns the report's `## 4. Follow-up Tasks` rows into `tasks/<task-group>/<new-task-id>/` stubs.
|
|
138
139
|
|
|
139
140
|
Behaviour contract:
|
|
140
141
|
- Idempotent: rows whose target dir exists are reported as `existing` and skipped. Reruns of the same parent task are safe.
|
|
@@ -149,7 +150,7 @@ The steps it executes, in this contractual order, and the contract each one carr
|
|
|
149
150
|
```
|
|
150
151
|
|
|
151
152
|
The status file is written after routing and follow-up persistence completes.
|
|
152
|
-
|
|
153
|
+
6. **`validate-run` — validate the finished run.** Checks the completed artifact set, including exact canonical-event-to-`agentActivity[]` conformance and the report-views contract that catches a missing or stale html sibling. A failure here names the specific contract; fix it and re-run `okstra report-finalize`.
|
|
153
154
|
|
|
154
155
|
After `okstra report-finalize` reports `"ok": true`, **execute the run-scoped cleanup gate.** Call `shutdown_workers` only after that success, all persistence work, and explicit user approval under [okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle". If the user keeps resources, leave the selected adapter's resources intact and surface its manual cleanup guidance.
|
|
155
156
|
|
|
@@ -164,6 +164,8 @@ After each worker attempt returns (regardless of role), Lead MUST verify the can
|
|
|
164
164
|
- The result file exists but its audit sidecar does not, at `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md`. Workers write both in the same step, so a result without a sidecar means the Reading Confirmation block — the only evidence the worker read its inputs — was never produced. `validate-run.py` fails the run on this at Phase 7 either way (`validate_worker_results_audit`); checking it here spends the existing one-retry budget while the role can still be re-dispatched, instead of surfacing hours later when the worker session is gone.
|
|
165
165
|
- `okstra worker-audit-check --run-dir <runs/<task-type>/> --task-type <t> --seq <n> --worker <id>` exits 2 on a backticked `path:line` citation in the worker's result that has no matching Evidence read row in its audit sidecar. Run it the moment you collect each result. Phase 7 enforces the same rules from the same implementation (`okstra_ctl.worker_audit_ledger`), but by then the worker session is gone and the only remaining moves are editing the result yourself — which destroys the audit chain the ledger exists to provide — or ending the run `contract-violated`. While the session is alive, `SendMessage` to the worker so it corrects its own citation; that costs about a minute against a re-dispatch or a failed run.
|
|
166
166
|
|
|
167
|
+
The same audit check parses command evidence from canonical rows such as `- Evidence command: {"command":"npm run check","cwd":"<project-root>","exitCode":0,"outputSummary":"all checks passed"}`. Workers record only commands that produced or verified a conclusion, not exploratory `rg`, `ls`, or file-opening commands. Environment variable values, tokens, credentials, and authorization headers are excluded. A malformed row or potential sensitive material is a contract failure returned by `okstra_ctl.worker_audit_ledger`; send the failure to the worker while its session is still available.
|
|
168
|
+
|
|
167
169
|
**One-retry policy:**
|
|
168
170
|
|
|
169
171
|
1. On the FIRST result-missing trigger for a given role within a single run, Lead MUST call `redispatch_worker` with the byte-identical prompt — same `**Result Path:**`, same `**Prompt History Path:**`, same model assignment, and same adapter-native assignment identity. The redispatch counts as a second attempt against the existing role slot; do NOT create a new role-id, do NOT change the result file path, do NOT switch to a different model as a "workaround".
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
- every `Kind=decision` clarification row carries its choices in `options[]`, never as prose inside `expectedForm`. Each option is an object with
|
|
1
|
+
- every `Kind=decision` clarification row carries its choices in `options[]`, never as prose inside `expectedForm`. Each option is an object with seven fields:
|
|
2
2
|
- `role` — `recommended` for the single best answer, `alternative` for the rest. Exactly one option per row is `recommended`.
|
|
3
3
|
- `answer` — the choice itself, phrased so the user can pick it as-is. Keep it to a short phrase (roughly 120 characters); the reasoning and the consequences have their own fields below.
|
|
4
4
|
- `rationale` — one sentence on why this option is on the board.
|
|
5
5
|
- `scopeImpact` — tokens drawn from `{in-repo, cross-repo, new-schema, deferrable}`. Exactly one of `in-repo` / `cross-repo`, which answer the same question and are mutually exclusive; `new-schema` and `deferrable` are optional additions.
|
|
6
6
|
- `addedWork` — one sentence naming the work this choice creates that the other choices do not. Name the work, not a cost adjective.
|
|
7
7
|
- `directionChange` — one sentence naming what this choice reverses: an approved plan item, a recorded decision, an earlier answer. When it reverses nothing, say so.
|
|
8
|
+
- `disposition` — the effect of selecting the option. Use `select` for `user-decision`, `accept-risk` for `noncritical-dissent`, and `request-revision` or `reject` when the option sends the plan back. `correctness-critical` never offers `accept-risk`.
|
|
9
|
+
- an approval-blocking row carries `approvalContext`. Its `classification` is one of `user-decision`, `noncritical-dissent`, or `correctness-critical`; `planItemIds`, `activityIds`, `unblockCondition`, and `recommendedDisposition` are required. A completed decision records `resolution` with `disposition`, non-empty `userText`, and verification `checkRefs`.
|
|
8
10
|
- the three impact fields answer three different questions — how far the change reaches, what new work it creates, and what it overturns. Someone choosing between options needs all three, so never fold them into one sentence: whichever axis is easiest to write would silently stand in for the other two.
|
|
9
11
|
- a row that omits `options[]`, offers fewer than two, or marks zero or two options as `recommended` is incomplete and must be completed before the report is finalised.
|
|
10
12
|
- `expectedForm` states only the *shape* of the answer — one of the options, a file path, a number, a date. It never lists the choices again; two sources for one fact leave consumers disagreeing about which is authoritative.
|
|
@@ -11,6 +11,7 @@ profile document.
|
|
|
11
11
|
- **Phase 4 / 5 (independent analysis)**: every analyser in the resolved provider assignment roster produces findings independently and has no access to another worker's output. `report-writer` does not analyse.
|
|
12
12
|
- **Phase 5.5 (convergence — peer review by workers)**: workers peer-review each other's findings across up to `effectiveMaxRounds` rounds; the lead mediates but does not vote. See `prompts/lead/convergence.md` for the round protocol (replay of findings, `AGREE` / `DISAGREE` / `SUPPLEMENT` verdicts), queue invariants, and final classification (`full-consensus` / `partial-consensus` / `contested` / `worker-unique`). For `requirements-discovery`, `error-analysis`, `implementation-planning`, `project-analysis`, `feature-analysis`, and `change-impact-analysis` this phase runs in **adversarial mode** (`convergence.adversarial=true`): verifiers try to refute each finding against its cited evidence and the burden of proof sits on the claim — see that skill's §"Adversarial Verification Mode".
|
|
13
13
|
- Do NOT conclude "no peer review happens" from the roster alone — every profile that lists ≥2 analyser workers runs convergence by default (`convergence.enabled=true` in `task-manifest.json`).
|
|
14
|
+
- For a new `implementation-planning` run, the plan-body sequence is initial verification → one planner self-fix → targeted re-verification → user gate. The initial verification is round 1, the targeted re-verification is round 2, and a second automatic self-fix is a contract violation. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop.
|
|
14
15
|
- **provider-unavailable fallback (tolerance).** A worker dispatch can fail to produce a result for two distinct reasons, and both take the same recovery path. (1) **Pane budget:** the dispatch is rejected with `no room for another tmux split` (or an equivalent teammate-pane creation failure). (2) **Sandbox CLI-start failure (non-tmux path):** an external CLI worker wrapper exits non-zero within seconds with empty stdout and its live-log shows `operation not permitted`. In either case the lead spends the one shared retry budget through the assignment's recorded runner. If the provider is still unavailable, record that terminal status and continue only under the convergence quorum rules; never replace it silently with a fixed provider or count a substitute as the original provider's vote. Completed external-CLI worker panes are reclaimed by the selected runtime adapter's resource lifecycle. (This is a prompt instruction, not a code-enforced gate.)
|
|
15
16
|
- Dual-audience final-report contract (shared):
|
|
16
17
|
- data.json is the sole authored report artifact. AI handoff Markdown and human HTML are independently derived from it; neither derived artifact is the other's source.
|
|
@@ -66,7 +67,7 @@ profile document.
|
|
|
66
67
|
- Schema-v2 final reports author `clarificationItems[]` in data.json; task-specific HTML renders the question and response controls directly from those IDs, and AI handoff Markdown renders the same array as one headed section per row for the next agent. The remaining table-layout rules describe schema-v1 compatibility and analysis-worker result tables only.
|
|
67
68
|
- **Every row that is still `open` and carries `Blocks=approval` records two more fields.** Withholding approval is the most expensive thing a report does to a run, and until these fields existed a blocker could not be told apart from a question nobody had put to the user.
|
|
68
69
|
- `origin` — who raised it. `worker-finding` (an analyser or verifier reached it on its own evidence), `material-gap` (neither the brief nor the codebase answers it), or `lead-directed` (the lead's own judgment, **including anything the lead instructed a worker to raise**). A lead that seeds its conclusion into a worker prompt and then reports the worker's agreement as an independent finding has mislabelled the row; that shape is what let one run block on a question its own lead had authored.
|
|
69
|
-
- `userConfirmation` — what happened before the row was written. `asked-and-answered`, `asked-awaiting` (asked, no answer yet), or `deferred-no-interactive-session` (this run had no user to ask).
|
|
70
|
+
- `userConfirmation` — what happened before the row was written. `asked-and-answered`, `asked-awaiting` (asked, no answer yet), or `deferred-no-interactive-session` (this run had no user to ask). Record an answer in `userInput` and move `status` to `answered`.
|
|
70
71
|
- Neither field is required once `status` is `answered` / `resolved` — the record lives in `userInput` by then.
|
|
71
72
|
- **Legacy canonical column schema (must match `templates/reports/final-report.template.md` §1 exactly):** every `## 1. Clarification Items` table has exactly these 4 columns, in this order:
|
|
72
73
|
`| <record-meta> | Statement | Expected form | User input |` (the first header is the i18n `columns.recordMeta` label — `Record`).
|