okstra 0.198.1 → 0.199.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/storage-model.md +10 -0
- package/docs/cli.md +4 -3
- package/docs/project-structure-overview.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/okstra-report-translate.py +28 -8
- package/runtime/prompts/lead/convergence.md +17 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +32 -6
- package/runtime/prompts/lead/plan-body-verification.md +41 -15
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +95 -12
- package/runtime/python/okstra_ctl/context_cost.py +16 -6
- package/runtime/python/okstra_ctl/direct_work.py +109 -0
- package/runtime/python/okstra_ctl/group_context.py +23 -3
- package/runtime/python/okstra_ctl/material.py +29 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +1 -0
- package/runtime/python/okstra_ctl/model_io/renderers.py +6 -0
- package/runtime/python/okstra_ctl/plan_items.py +39 -5
- package/runtime/python/okstra_ctl/plan_items_cli.py +61 -6
- package/runtime/python/okstra_ctl/recap.py +6 -0
- package/runtime/python/okstra_ctl/render.py +8 -4
- package/runtime/python/okstra_ctl/report_assembly.py +20 -2
- package/runtime/python/okstra_ctl/report_narrative.py +4 -4
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +49 -40
- package/runtime/python/okstra_ctl/report_translation.py +19 -0
- package/runtime/python/okstra_ctl/run.py +5 -0
- package/runtime/python/okstra_ctl/set_work_status.py +90 -40
- package/runtime/python/okstra_ctl/task_list_cli.py +2 -0
- package/runtime/python/okstra_ctl/user_response.py +1 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +5 -1
- package/runtime/python/okstra_project/state.py +4 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10 -0
- package/runtime/skills/okstra-inspect/SKILL.md +6 -1
- package/runtime/skills/okstra-inspect/facets/recap.md +6 -1
- package/runtime/skills/okstra-inspect/facets/status.md +12 -0
- package/runtime/templates/reports/html/assets/base.css +4 -0
- package/runtime/templates/reports/html/assets/base.js +30 -0
- package/runtime/validators/validate-run.py +47 -6
- package/runtime/validators/validate_session_conformance.py +5 -0
|
@@ -242,6 +242,16 @@ Path segments are normalized into slugs. If a slug would be empty, as can happen
|
|
|
242
242
|
|
|
243
243
|
## Task manifest contract
|
|
244
244
|
|
|
245
|
+
`okstra set-work-status` can register an existing brief without creating a run. Such a task
|
|
246
|
+
has `registrationSource: direct`, its identity and brief path, and user-managed `workStatus`.
|
|
247
|
+
No workflow phase or run status is synthesized. A direct completion stores its work and
|
|
248
|
+
verification summary in `<task-root>/work-records/<digest>.json`; `latestWorkRecordPath`
|
|
249
|
+
points to it and each record's `previousRecordPath` preserves earlier completions. Actual
|
|
250
|
+
run preparation preserves these fields. Group memory records the direct source and result
|
|
251
|
+
path; status, recap, and later run inputs expose that path without treating it as a final report.
|
|
252
|
+
The command checks required completion summaries and reports sharing failures separately
|
|
253
|
+
from saved state (`tests/inspect/test_okstra_set_work_status.py`).
|
|
254
|
+
|
|
245
255
|
`task-manifest.json` is the canonical metadata file the active Okstra lead uses to understand task continuity.
|
|
246
256
|
As a general rule, this manifest records the absolute `projectRoot` path only once and expresses the remaining generated paths as project-relative fields whenever possible.
|
|
247
257
|
|
package/docs/cli.md
CHANGED
|
@@ -816,8 +816,9 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
816
816
|
| `okstra plan-items <prepare\|prompt\|validate-prepared> --run-manifest <path> …` | Bind the implementation-planning verification queue to the run manifest. `prepare` extracts the exact queue from `--narrative` and, when `designPreparation.mode` is `no-design-inputs` and the Stage Map has one row, flips `convergence.planBodyVerification.gating` to `false` (stdout `Gating`). `prompt` emits its fixed lossless view, ending with the parser-facing `## Response format` block so the block reaches every verifier with the queue; `validate-prepared` proves the prepared queue still matches the narrative. With `--state <plan-body-verification.json>` the round is a re-verification: `prepare` also carries each queued item's recorded votes and `selfFixNote` into the envelope as `priorRounds`, `prompt` renders them as that item's `**Prior round dissent**` block behind a re-verification preamble, and `validate-prepared` re-derives the carry and rejects an envelope that dropped it. Python resolves the convergence-owned state path, so model callers never choose it. |
|
|
817
817
|
| `okstra plan-items seed --narrative <report-narrative.md> --state <plan-body-verification.json> [--prior-state <previous plan-body-verification.json>]` | Create the convergence-owned `planBodyVerification.planItems[]` rows every verdict lands in, from the same deterministic extraction `extract` uses. The historical v2 form is `--data <data.json>`. Idempotent by id: an existing row keeps its verdicts and carried fields. Reports `seeded` / `existing` counts. `--prior-state` carries the previous **run**'s verdicts into this one: a newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its `verdicts[]` and is tagged `carriedForwardFromSeq` with the seq read off the prior filename, so round 1 does not re-judge text nobody changed. A matching id alone never carries — `P-*` ids are positional and shift. It requires `--state`, refuses a prior state whose task root differs from the one `--state` lives under (the state file carries no task identity, so its path is the only identity there is), and when it carries anything it rewrites the sibling `plan-items-*.json` `dispatchQueue` the way `incremental-carry` does. Adds `carried` / `carriedForwardFromSeq` to the reported counts. |
|
|
818
818
|
| `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md>… --round <N>` | Read each worker's Markdown verdict directly, validate every current `P-*` id, reject duplicate worker submissions, and overwrite that round's stored verdicts. `--items <plan-items artifact>` narrows the assignment to what a partial round actually dispatched — pass the `--tie-vote` artifact on a critic tie round, or every item the critic was never given is refused as unanswered. The unanswered check still applies inside the narrowed set. The historical `--verdicts <file>` form remains automation compatibility only. Without `--append` every recorded verdict row of the queued items is replaced; when a row belongs to a round `complete-round` never closed, the command refuses before writing and names the `complete-round --round <M>` to run first. `--discard-open-rounds` replaces anyway — the recovery path when those rounds are being re-applied from their result files in order (the discarded rows are printed); it also restores `dispatchQueue` to the items those result files answer, since the persisted queue belongs to the latest round and an earlier round's verdicts would otherwise be refused as outside it. |
|
|
819
|
-
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched. Models do not write the state JSON. Stdout also carries `nextDispatch`. |
|
|
820
|
-
| `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1 on a run that rostered a critic, `critic-worker` on those item ids only. `kind: user-decision` — the same 1-1 on a run with no critic rostered: no in-band vote can break it, so open one `okstra approval-decision open` per item (classification `noncritical-dissent`) plus its `## 1. Clarification Items` row and dispatch no further verification for them. `--run-manifest` is what tells the two apart (`invocationAssignments` `critic/*`); without it the answer stays `critic-tie`. A missing path is never environment-unverifiable. |
|
|
819
|
+
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched. Models do not write the state JSON. Stdout also carries `nextDispatch`. A second automatic rewrite is rejected; the one rewrite may contain several cause groups. `selfFixRoundsApplied` remains its verification round number, not a rewrite count. |
|
|
820
|
+
| `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1 on a run that rostered a critic, `critic-worker` on those item ids only. `kind: user-decision` — the same 1-1 on a run with no critic rostered: no in-band vote can break it, so open one `okstra approval-decision open` per item (classification `noncritical-dissent`) plus its `## 1. Clarification Items` row and dispatch no further verification for them. `--run-manifest` is what tells the two apart (`invocationAssignments` `critic/*`); without it the answer stays `critic-tie`. A missing path is never environment-unverifiable. After the single automatic self-fix, remaining items route to `lead-decision` first, then `user-decision`, with no further automatic worker batch. |
|
|
821
|
+
| `okstra plan-items resolve-dissent --state <plan-body-verification.json> --item <P-id> --decision-file <markdown-file>` | Record the lead decision, its authority within agreed scope, and cited evidence after the one self-fix. Accepts only current, noncritical design judgements; facts, user-input requirements and non-results require user confirmation. Keeps original votes and appends the decision to the report dissent log. The decision expires when its content, scope or verdicts change. |
|
|
821
822
|
| `okstra plan-items correction-prompt --state <plan-body-verification.json> --run-manifest <path> --worker <id>` | Emit the planning-time environment-gap paragraph, then the assigned queue. The environment exception is first. Exits 2 unless `next-dispatch` named that worker as a blanket-UNVERIFIABLE correction target. |
|
|
822
823
|
| `okstra error-log append-observed --out <errors.jsonl> --task-key <key> --phase <task-type> --agent <assigned-worker-id> --agent-role worker --model <model> --error-type tool-failure --command-file <markdown-file> --command-kind <kind> --message-file <markdown-file> [--cause <cause> --evidence-file <kind=file>]…` | Worker-facing typed error recording surface. Python validates and serializes the JSONL record; a worker supplies scalar identity fields plus Markdown files for free-form command, message, and probe content, never a JSON sidecar or JSON argument. `sandbox-denied` requires both `targetProbe` and `controlProbe` evidence files. |
|
|
823
824
|
| `okstra config <get\|set\|unset\|show> [key] [value] [--scope project\|global\|all]` | Manage persistent settings such as `pr-template-path` with atomic JSON writes |
|
|
@@ -844,7 +845,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
844
845
|
| `okstra incremental-scope <args…>` | Decide re-verify vs carry-forward scope for an `implementation-planning` clarification re-run. Thin shim into `scripts/okstra_ctl/incremental_scope.py` (deterministic): it reads the dependency graph from the prior run `data.json`'s `implementationPlanning.stageMap` and returns `mode:"incremental"` only when the base-ref SHA is unchanged and the affected stages' `downstream_stage_closure` covers at most half of all stages; `--full-reason` (selected option / Stage Map / approach) still forces `mode:"full"`. An answered `C-NNN` that traces to no stage returns `mode:"unresolved"` rather than full — pass `--impacted` with the stage numbers or `--full-reason`. `--run-manifest <path>` is required for a decision (not for `--preview`): the same decision is written to the record that manifest names in `incrementalDecisionPath`, so the report writer's authoring contract and `okstra incremental-carry` read it instead of CSVs the lead re-typed. `mode: "unresolved"` is a question back to the lead and is deliberately not recorded. `--preview --prev-data <path> --answered-clarifications <csv>` runs the link half alone — no base SHA, no side effects — and prints `{wouldForceFull, unlinkedIds, reason}`; unlinked ids set `wouldForceFull: false` and fill `unlinkedIds` |
|
|
845
846
|
| `okstra incremental-carry <args…>` | Merge carried-forward plan-item verdicts into an incremental re-run. Contract v3 takes `--prev-data`, `--cur-narrative`, and the convergence-owned `--state`; it verifies carried stage rows and writes only `--out-state`, tagging copied verdicts with `carriedForwardFromSeq`. Pass `--decision <incrementalDecisionPath>` for the two stage sets — the same record the report writer's authoring contract was built from — instead of `--carry-stages` / `--reverify-stages`, which the lead re-typed off stdout; the two forms cannot be combined, and a record whose `mode` is not `incremental` is refused. Unchanged `P-Val-*` / `P-Req-*` / `P-Rb-*` rows whose extract hash still matches are carried the same way, and the sibling `plan-items-*.json` `dispatchQueue` is rewritten to match. The historical v2 `--cur-data --out` form remains readable. Ownership, scope, item, or schema drift raises `CarryError` and forces a full fallback. |
|
|
846
847
|
| `okstra code-review target --task-key <k> --stage <N> [--project-root <dir>] [--cwd <dir>] [--json]` / `okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] [--cwd <dir>] [--json]` | Resolve what a code review reads and where its result file goes. Output is always JSON, so `--json` only makes that explicit. `--project-root` and `--cwd` are shared pre-dispatch arguments and apply to both modes; `--cwd` is only consulted when `--project-root` is absent. Both modes return `{ ok, projectRoot, mode, worktreePath, branch, baseCommit, headCommit, reviewPath, round }`; stage mode additionally returns `taskKey`, `taskRoot`, and `stage`. Stage mode takes the diff base from the `base_ref` recorded on that stage's worktree-registry row when it was provisioned — not from a rule re-applied at review time — and names the result `.okstra/tasks/<task-group>/<task-id>/code-reviews/stage-<NN>.md`, where a re-review of the same stage becomes `-r2`, `-r3`, … (the `round` field). Only a legacy row provisioned before `base_ref` was recorded falls back to re-deriving the base through `stage_targets`, and a failure there is reported as `stage_base_unresolved`. `worktreePath` comes back empty whenever the stage worktree is not usable as a live checkout — the registry row is no longer `active` (whole-task final-verification released it), the row never carried a path, or the recorded directory is gone — and the review then reads the `branch` ref instead. Branch mode uses `--base` when given, otherwise the merge-base with the default branch (`refs/remotes/origin/HEAD`, else `main`/`master`), and names the result `.project-docs/code-reviews/<branch>/<YYYY-MM-DD>-<NN>.md`, where `<NN>` (the `round` field) is the next sequence number for that date — the highest already on disk plus one. Read-only: it resolves paths and creates no directory and no file, so the review directory does not exist until the caller writes the report. Backend for the okstra-code-review skill |
|
|
847
|
-
| `okstra set-work-status <token> <todo\|in-progress\|blocked\|done> [--note <text>] [--task-group <g>] [--project-root <dir>]` | Update user-managed `workStatus
|
|
848
|
+
| `okstra set-work-status <token> <todo\|in-progress\|blocked\|done> [--note <text> \| --note-file <path>] [--task-group <g>] [--project-root <dir>]` | Update user-managed `workStatus`. Existing briefs can be registered without a run. Direct completion requires a work and verification summary; use a UTF-8 note file for longer results. Records are shared through group context, inspect, and subsequent runs without claiming cross-verification. Ambiguous IDs return `matches[]`. A `share` failure with `statusRecorded:true` can be retried with the same command. |
|
|
848
849
|
| `okstra worktree-lookup <project-id> <task-group> <task-id>` | Return the `worktree_registry.lookup` result: reserved path, branch, base ref, and current status |
|
|
849
850
|
| `okstra worktree-status [--path <dir>] [--check-clean]` | Answer "is this worktree clean?" over source paths only, excluding what okstra provisioned there — `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and any nested stage worktree. A bare `git status --porcelain` in a task worktree is never empty for that reason, so a plan step asserting a clean tree with one fails on okstra's scaffolding instead of on the stage's own work; this is the same gate `handoff` and stage integration use. Output is JSON `{ ok, path, clean, entries, excluded }` where `entries` holds the `git status --short` rows that made it dirty. Exit code is 0 regardless unless `--check-clean` is given, which exits 1 on a dirty tree so it can stand as a shell assertion (`okstra worktree-status --check-clean`). okstra writes `stage-<N>-exit` itself when it settles the stage, so a plan step must not tag. A path outside a git work tree exits 2 rather than reporting a clean tree |
|
|
850
851
|
| `okstra plan-validate <plan-path>` | Run `_validate_approved_plan` and report frontmatter `approved` recognition plus unresolved Blocks=approval rows |
|
|
@@ -178,7 +178,7 @@ The Module column below is where the command's behaviour lives — a `src/` modu
|
|
|
178
178
|
| `integrate-stages` | `scripts/okstra_ctl/stage_integrate.py` | Merge verified stages into the task worktree and clean stage worktrees |
|
|
179
179
|
| `task-list`, `task-show` | `scripts/okstra_ctl/task_list_cli.py`, `scripts/okstra_ctl/task_show_cli.py` | Task/run introspection for skills; `task-show` consumes the Python task read-side snapshot |
|
|
180
180
|
| `resolve-task-key` | `scripts/okstra_ctl/resolve_task_key.py` | Resolve a bare task-id to candidate task-keys from the project catalog |
|
|
181
|
-
| `set-work-status` | `scripts/okstra_ctl/set_work_status.py` | Set
|
|
181
|
+
| `set-work-status` | `scripts/okstra_ctl/set_work_status.py`, `scripts/okstra_ctl/direct_work.py` | Set user-managed `workStatus`, register an existing brief without a run, and share direct completion records through group memory and task queries |
|
|
182
182
|
| `time-report`, `log-report`, `error-report`, `error-zip` | `scripts/okstra_ctl/time_report.py`, `scripts/okstra_ctl/log_report.py`, `scripts/okstra_ctl/error_report.py`, `scripts/okstra_ctl/error_zip.py` | Read-side task runtime, wrapper log, and error aggregation helpers |
|
|
183
183
|
| `run-audit` | `scripts/okstra_ctl/run_audit.py` | Anomaly detection — checks run artifacts against progress invariants and reports invariant violations, read-only (Python: `okstra_ctl.run_audit`) |
|
|
184
184
|
| `worker-liveness` | `scripts/okstra_ctl/worker_liveness.py` | Report whether pending workers are still alive, so the lead's poll ends a stalled wait early instead of paying the deadline (Python: `okstra_ctl.worker_liveness`) |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -32,7 +32,11 @@ from okstra_bootstrap import prefer_colocated_modules # noqa: E402
|
|
|
32
32
|
|
|
33
33
|
prefer_colocated_modules(__file__, "okstra_ctl/report_translation.py")
|
|
34
34
|
|
|
35
|
-
from okstra_ctl.report_translation import
|
|
35
|
+
from okstra_ctl.report_translation import ( # noqa: E402
|
|
36
|
+
extract,
|
|
37
|
+
group_translation_pointers,
|
|
38
|
+
overlay,
|
|
39
|
+
)
|
|
36
40
|
from okstra_ctl.final_report_paths import ( # noqa: E402
|
|
37
41
|
translation_sidecar_path,
|
|
38
42
|
translation_source_path,
|
|
@@ -82,6 +86,7 @@ def _report_snapshot(path: Path):
|
|
|
82
86
|
def _source_payload(
|
|
83
87
|
authority: RunArtifactAuthority, data_path: Path, raw_bytes: bytes, data: dict
|
|
84
88
|
) -> dict:
|
|
89
|
+
strings = extract(data)
|
|
85
90
|
return {
|
|
86
91
|
"taskKey": authority.task_key,
|
|
87
92
|
"runManifestPath": authority.manifest_ref,
|
|
@@ -89,7 +94,8 @@ def _source_payload(
|
|
|
89
94
|
"sourceDataPath": data_path.relative_to(authority.project_root).as_posix(),
|
|
90
95
|
"sourceDataSha256": hashlib.sha256(raw_bytes).hexdigest(),
|
|
91
96
|
"lang": str((data.get("meta") or {}).get("reportLanguage") or ""),
|
|
92
|
-
"strings":
|
|
97
|
+
"strings": strings,
|
|
98
|
+
"translationGroups": group_translation_pointers(data, strings),
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
|
|
@@ -108,6 +114,10 @@ def _validate_source_payload(
|
|
|
108
114
|
raise SystemExit(
|
|
109
115
|
"error: report changed after translation source publication"
|
|
110
116
|
)
|
|
117
|
+
if "translationGroups" in source and (
|
|
118
|
+
source["translationGroups"] != expected["translationGroups"]
|
|
119
|
+
):
|
|
120
|
+
raise SystemExit("error: translation groups do not match report source")
|
|
111
121
|
|
|
112
122
|
|
|
113
123
|
def cmd_extract(args: argparse.Namespace) -> int:
|
|
@@ -154,9 +164,13 @@ def cmd_source(args: argparse.Namespace) -> int:
|
|
|
154
164
|
print(line("Report", payload["sourceDataPath"]), end="")
|
|
155
165
|
print(line("Run manifest", authority.manifest_ref), end="")
|
|
156
166
|
print(line("Source digest", payload["sourceDataSha256"]), end="")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
groups = payload["translationGroups"]
|
|
168
|
+
print(line("Field count", len(strings)), end="")
|
|
169
|
+
print(line("String count", len(groups)), end="")
|
|
170
|
+
print(line("Source characters", sum(len(value) for value in strings.values())), end="")
|
|
171
|
+
print(line("Translation characters", sum(len(strings[row[0]]) for row in groups)), end="")
|
|
172
|
+
for index, pointers in enumerate(groups, 1):
|
|
173
|
+
print(f"\n## T-{index:03d}\n{block(strings[pointers[0]])}")
|
|
160
174
|
return 0
|
|
161
175
|
|
|
162
176
|
|
|
@@ -212,12 +226,14 @@ def cmd_write(args: argparse.Namespace) -> int:
|
|
|
212
226
|
source = _load(source_path)
|
|
213
227
|
_validate_source_payload(source, expected, args.source_digest)
|
|
214
228
|
sources = expected["strings"]
|
|
229
|
+
# 발행된 구형 작업 목록은 필드당 한 블록이므로 진행 중 번역의 번호를 보존한다.
|
|
230
|
+
groups = source.get("translationGroups", [[pointer] for pointer in sources])
|
|
215
231
|
translated = _translation_blocks(Path(args.translations))
|
|
216
|
-
if len(translated) != len(
|
|
232
|
+
if len(translated) != len(groups):
|
|
217
233
|
raise SystemExit(
|
|
218
234
|
f"error: translation blocks must match every T-NNN item — "
|
|
219
235
|
f"{len(translated)} blocks in {Path(args.translations).name}, "
|
|
220
|
-
f"{len(
|
|
236
|
+
f"{len(groups)} items in the translation source"
|
|
221
237
|
)
|
|
222
238
|
empty = [f"T-{index + 1:03d}" for index, value in enumerate(translated) if not value]
|
|
223
239
|
if empty:
|
|
@@ -229,7 +245,11 @@ def cmd_write(args: argparse.Namespace) -> int:
|
|
|
229
245
|
sidecar = translation_sidecar_path(data_path, lang)
|
|
230
246
|
if sidecar.is_symlink():
|
|
231
247
|
raise SystemExit("error: translation sidecar path is a symlink")
|
|
232
|
-
strings =
|
|
248
|
+
strings = {
|
|
249
|
+
pointer: value
|
|
250
|
+
for pointers, value in zip(groups, translated)
|
|
251
|
+
for pointer in pointers
|
|
252
|
+
}
|
|
233
253
|
_, report = overlay(data, strings)
|
|
234
254
|
if report.unresolved or report.applied != len(strings):
|
|
235
255
|
raise SystemExit("error: translations do not validate against report source")
|
|
@@ -334,6 +334,23 @@ Lightweight reverify does not require the original `analysis-packet.md`, `analys
|
|
|
334
334
|
|
|
335
335
|
This is the single largest avoidable cost in `requirements-discovery`, `error-analysis`, `implementation-option-selection`, and `implementation-planning` runs. Treat as mandatory.
|
|
336
336
|
|
|
337
|
+
## Conditional reference reading
|
|
338
|
+
|
|
339
|
+
The common read retains finding classification, queue pruning, dispatch gates,
|
|
340
|
+
state ownership, and output rules. Use the generated `okstra convergence
|
|
341
|
+
reverify-prompt` body for every verifier. The following reads are guidance for
|
|
342
|
+
avoiding unused examples; queue and dispatch validators still enforce execution.
|
|
343
|
+
|
|
344
|
+
| Current operation | Additional section to read |
|
|
345
|
+
|---|---|
|
|
346
|
+
| Diagnosing a rejected verifier prompt | The reference prompt matching the selected verification mode |
|
|
347
|
+
| `config.critic.enabled` is true | Coverage critic pass, including its shared dispatch procedure |
|
|
348
|
+
| An enabled critic uses acceptance mode for `final-verification` | Acceptance critic pass as well as the shared Coverage critic dispatch procedure |
|
|
349
|
+
|
|
350
|
+
Read the matching sections before their dispatch. A disabled critic needs neither
|
|
351
|
+
critic section. Generated prompts retain the selected mode's instructions even
|
|
352
|
+
when the lead does not read the example text.
|
|
353
|
+
|
|
337
354
|
### Lightweight Re-verification Prompt
|
|
338
355
|
|
|
339
356
|
Rendered by `okstra convergence reverify-prompt` when `config.adversarial` is false; the block below is the reference shape, and the rendered body additionally carries each finding's `**Origin item**` and `**Origin audit sidecar**` lines.
|
|
@@ -22,8 +22,8 @@ This document is the operating contract and phase index. Detailed procedures liv
|
|
|
22
22
|
|-------|-------|
|
|
23
23
|
| [context-loader](./context-loader.md) | Phase 1 task-bundle discovery, manifest fields, run-directory layout |
|
|
24
24
|
| [team-contract](./team-contract.md) | Phase 2–5 worker roster, model assignment rules, prompt composition (anchor headers, `[Required reading]`, `[Error reporting]`), worker output contract, terminal statuses, usage tracking |
|
|
25
|
-
| [convergence](./convergence.md) | Phase 5.5 finding convergence loop, finding categories, reverify dispatch
|
|
26
|
-
| [plan-body-verification](./plan-body-verification.md) | Phase 6 plan-body verification sub-step (implementation-planning only) — plan-item extraction, verdict semantics, gate resolution, state schema. Read only at that sub-step |
|
|
25
|
+
| [convergence](./convergence.md) | Phase 5.5 finding convergence loop, finding categories, reverify dispatch, convergence state schema. Use the bounded common read under Doctrine lazy reads |
|
|
26
|
+
| [plan-body-verification](./plan-body-verification.md) | Phase 6 plan-body verification sub-step (implementation-planning only) — plan-item extraction, verdict semantics, gate resolution, state schema. Read only the common procedure at that sub-step, using the bounded read below |
|
|
27
27
|
| [report-writer](./report-writer.md) | Phase 6 final-report authorship, dispatch template, resume-safe dispatch, shared-graph integrity check, Phase 7 token-usage collector |
|
|
28
28
|
|
|
29
29
|
Read-side inspection (`/okstra-inspect`) and scheduling (`/okstra-schedule-gen`) are user-invoked skills, not lead support contracts — the lead does not consult them during a run.
|
|
@@ -262,11 +262,23 @@ After context-loader completes, read **only the compact intake files below** in
|
|
|
262
262
|
|
|
263
263
|
**Doctrine lazy reads (BLOCKING — read at the round, not at Phase 1):**
|
|
264
264
|
|
|
265
|
-
- [convergence](./convergence.md) — read before the first `PROGRESS: phase-5.5-convergence` line of any run that runs a convergence round.
|
|
266
|
-
- [plan-body-verification](./plan-body-verification.md) — read before the first `PROGRESS: phase-5.5.9-plan-verify` line.
|
|
265
|
+
- [convergence](./convergence.md) — read the common procedure with the bounded command below before the first `PROGRESS: phase-5.5-convergence` line of any run that runs a convergence round.
|
|
266
|
+
- [plan-body-verification](./plan-body-verification.md) — read the common procedure with the bounded command in the Phase 6 sub-step before the first `PROGRESS: phase-5.5.9-plan-verify` line.
|
|
267
267
|
|
|
268
268
|
Both stay out of the Phase 1 baseline for the token reason above, and neither is optional at its round: together they carry more than half of this contract family's MUST clauses, so a round dispatched without the read is a round run from memory. **Enforced:** `validators/validate_session_conformance.py` `_ENTRY_GUARD_READS` requires each read — a `Read` call or a shell command naming the file — inside this run's window and before that checkpoint. The requirement is conditioned on the checkpoint actually appearing, so a run that holds no such round is never asked for it.
|
|
269
269
|
|
|
270
|
+
Read the convergence common procedure from the resolved resource path. Replace
|
|
271
|
+
`<convergence-contract-path>` with its absolute path. This skips reference prompt
|
|
272
|
+
examples and critic-specific procedures; the conditional reading table in the
|
|
273
|
+
output identifies when those sections are needed:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
awk '/^### Lightweight Re-verification Prompt$/ {skip=1} /^## Convergence State Artifact$/ {skip=0} /^## Coverage critic pass$/ {skip=1} /^## Output$/ {skip=0} !skip {print}' '<convergence-contract-path>'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`tests/contract/test_contract_examples_execute.py::test_convergence_common_read_keeps_protocol`
|
|
280
|
+
checks the emitted common procedure. The same entry-guard read trace applies.
|
|
281
|
+
|
|
270
282
|
**Lazy reading discipline (do NOT read at Phase 1):**
|
|
271
283
|
|
|
272
284
|
- `task-index.md` — only when the user explicitly asks for a human summary or when history disambiguation is required.
|
|
@@ -429,7 +441,21 @@ After the Report writer worker narrative is reviewed, **if** `task_type == "impl
|
|
|
429
441
|
|
|
430
442
|
This is a Phase 6 sub-step — it does NOT introduce a new top-level lifecycle phase; the lead operating-phase model (Phase 1 Intake → Phase 7 Persist, labels in the "Quick Reference" table above as the single source of truth) is preserved. The round's outcome is read from the final report's `### 5.5.9 Plan Body Verification` section and `implementationPlanning.planBodyVerification` in its data.json — it is not a separate lifecycle phase identifier.
|
|
431
443
|
|
|
432
|
-
**REQUIRED RESOURCE:** Read [plan-body-verification](./plan-body-verification.md) for the round protocol, plan-item ID scheme (`P-Dir-1` for selected-direction; `P-Opt-*` for legacy candidate comparison; then `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` / `P-Req-*` / `P-Prep-*`), verdict semantics (`AGREE` / `DISAGREE(a-f)` / `SUPPLEMENT`), classification rules, gate-result resolution, and the state-file schema
|
|
444
|
+
**REQUIRED RESOURCE:** Read the common procedure of [plan-body-verification](./plan-body-verification.md) for the round protocol, plan-item ID scheme (`P-Dir-1` for selected-direction; `P-Opt-*` for legacy candidate comparison; then `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` / `P-Req-*` / `P-Prep-*`), verdict semantics (`AGREE` / `DISAGREE(a-f)` / `SUPPLEMENT`), classification rules, gate-result resolution, and state-path authority. Read the state-file schema only when diagnosing state or projection validation. For `P-Dir-1`, compare `directionRealization` with `selectedDirectionRef` and its snapshot: verify the core mechanism, architecture boundaries, planning invariants, and any hidden direction change.
|
|
445
|
+
|
|
446
|
+
Read from the resolved runtime resource path, replacing `<plan-body-contract-path>`
|
|
447
|
+
with that resource's absolute path. This prints the complete common procedure and
|
|
448
|
+
its conditional reading table, stopping before reference examples:
|
|
449
|
+
|
|
450
|
+
```sh
|
|
451
|
+
awk '/^## Reference material$/ {exit} {print}' '<plan-body-contract-path>'
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Follow the conditional reading table for later rounds and validation failures.
|
|
455
|
+
The command keeps the filename in the read trace used by
|
|
456
|
+
`validators/validate_session_conformance.py` `_ENTRY_GUARD_READS`.
|
|
457
|
+
The bounded output is checked by
|
|
458
|
+
`tests/contract/test_contract_examples_execute.py::test_plan_body_common_read_keeps_gate_rules`.
|
|
433
459
|
|
|
434
460
|
Distinct from Phase 5.5 finding convergence:
|
|
435
461
|
|
|
@@ -439,7 +465,7 @@ Distinct from Phase 5.5 finding convergence:
|
|
|
439
465
|
|
|
440
466
|
Lead's responsibilities in this sub-step (in order):
|
|
441
467
|
|
|
442
|
-
For a new `implementation-planning` run, the fixed order is initial verification → one planner self-fix → targeted re-verification → user
|
|
468
|
+
For a new `implementation-planning` run, the fixed order is initial verification → one planner self-fix → targeted re-verification → lead decision or immediate user confirmation. The initial verification is round 1 and the targeted re-verification is round 2. A second automatic self-fix is a contract violation. When `okstra plan-items prepare` reports `"gating": false` (one-stage `no-design-inputs` plan), skip the self-fix loop and the sweep batch: extraction and round 1 still run, then go to the user gate. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating: true` and the full order.
|
|
443
469
|
|
|
444
470
|
1. Build the queue with `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, place the output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then run `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. Python resolves the one convergence-owned state path from that run identity. 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:
|
|
445
471
|
|
|
@@ -48,7 +48,7 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
|
|
|
48
48
|
|---------|---------|-------------|
|
|
49
49
|
| `enabled` | `true` | If `false`, the round is skipped and the approval gate is not blocked by this round (legacy behaviour). |
|
|
50
50
|
| `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
|
|
51
|
-
| `selfFixMaxRounds` | `1` |
|
|
51
|
+
| `selfFixMaxRounds` | `1` | Fixed limit, not configurable: one automatic report-writer rewrite at most. Skip when nothing is fixable. `plan_items_cli._record_self_fixes`, `validate-run._validate_self_fix_grouping`, and session activity validation enforce the limit. |
|
|
52
52
|
| `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. Prepare emits `true` because the plan does not exist yet. After the report-writer draft, `okstra plan-items prepare` (and `seed`) flip it to `false` when `designPreparation.mode` is `no-design-inputs` and the Stage Map has exactly one row. That path keeps extraction and one verification round and does not run the self-fix loop or a sweep batch. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating=true`. `--no-plan-verification` is the separate manual opt-out (`enabled=false`). **Enforced:** `okstra_ctl.plan_items.advisory_plan_body_gating`, `validators/validate-run.py` `_validate_advisory_plan_body_gating`. "never blocks approval" is enforced by the `gating is False` early return in `_recompute_plan_body_gate`, `_gate_blocking_causes`, `_validate_plan_body_clarification_matching`, and `_validate_self_fix_before_clarification` — an advisory round neither demands a `blocks=approval` row nor an exhausted self-fix budget. |
|
|
53
53
|
|
|
54
54
|
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. `gating=false` is not that opt-out: extraction and one round still run.
|
|
@@ -376,7 +376,7 @@ round before any host or provider process starts.
|
|
|
376
376
|
**On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its verdicts, is tagged `carriedForwardFromSeq`, and drops out of round 1's `dispatchQueue` — in the state and in the sibling `plan-items-*.json` the next `okstra plan-items prompt` reads. Without it a re-run re-judges every line the previous run already settled, and the same dissent re-opens under a new `P-*` number. A matching id alone never carries: `P-*` ids are positional, so a plan that gained one line hands the old id to a different sentence. The flag requires `--state` and refuses a prior state file that sits under another task's root — a plan-body state carries no task identity of its own, so its path is the only identity there is. **Enforced:** `okstra_ctl.plan_items_cli._carry_prior_run_verdicts`.
|
|
377
377
|
|
|
378
378
|
**`--run-manifest` is what scopes the gate to the stage you are starting.** Seed uses it to overlay disk `done` / `active` onto `planBodyVerification.stageLedger`. The current plan's depends-on fills `ready` / `blocked` when no prior plan exists, so a first run does not treat every stage as in-scope. **Enforced:** `okstra_ctl.plan_items.planning_stage_ledger`.
|
|
379
|
-
2. For each analyser worker in the roster
|
|
379
|
+
2. For each analyser worker in the roster, use `okstra plan-items prompt` output verbatim as the instruction body in the materialization sequence above. Read §"Re-verification rounds (round 2+)" before preparing a later round; read §"Plan-body reverify prompt" only when diagnosing a prompt-contract failure.
|
|
380
380
|
3. Dispatch uses the same wrapper infrastructure as finding convergence, so the `--role-slug` is the same canonical `<role>-worker` that convergence uses — not a round-specific slug. Result file path: `runs/<task-type>/worker-results/<role>-worker-plan-verify-r<N>-implementation-planning-<seq>.md` (e.g. `codex-worker-plan-verify-r1-implementation-planning-003.md`). **`<seq>` is the report's sequence** — the one in this run's `final-report-<task-type>-<seq>` filename, NOT the `workerResults` sequence the initial analysis results carry. The two are equal in most runs and diverge in some (`reports: 004` alongside `workerResults: 005` is a real case). Provenance no longer globs by either seq: it resolves the expected filenames from this run's team-state `workerDispatches[]` rows — the paths the dispatches actually recorded — and falls back to the seq glob only when no team-state is readable, saying so in its finding. The `-worker-` token is load-bearing twice over: §"Plan-body reverify prompt" requires the same anchor headers as convergence, whose `**Audit sidecar path:**` is derived by `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()` inserting `-audit-` after that token — a slug without it makes the header underivable and the helper raises. Record each `planItems[].verdicts[].worker` as the same `<role>-worker` string, because provenance compares it to this filename's prefix. **Enforced:** `tests/contract/test_reverify_dispatch_anchors.py` derives the sidecar from the documented name and re-extracts the prefix the provenance resolver uses.
|
|
381
381
|
**Verdict provenance.** Every verdict recorded in `planItems[].verdicts[]` MUST trace back to a dispatch that actually returned a result file. The whole gate — classification, self-fix eligibility, promotion, `gateBlockedBy` — is computed from these votes, so an unbacked vote lets the round be skipped while the gate still reads `passed`. **Enforced (advisory):** `validators/validate-run.py` `_validate_plan_body_verdict_provenance` reports any `verdicts[].worker` with no recorded reverify dispatch whose result file exists; the finding is not in the blocking allowlist, so it surfaces as an advisory rather than failing the run — record it, never dismiss it. Recording a `verification-error` for a dispatch that produced no result is the correct way to represent a failed worker — inventing an `AGREE` is a contract violation, and renaming a result file to make provenance match destroys the link the check reads.
|
|
382
382
|
|
|
@@ -423,7 +423,8 @@ round before any host or provider process starts.
|
|
|
423
423
|
| `worker-correction` | Re-dispatch **only** those workers. Peers are not re-run. The queue does not become a new round. Place the output of `okstra plan-items correction-prompt --worker <id> --run-manifest … --state …` first in that worker's prompt — the environment-exception paragraph is first. A byte-identical re-dispatch reproduces the same failure; a corrected one recovered 37 substantive verdicts from a worker whose first attempt answered `UNVERIFIABLE` to all 80 items. |
|
|
424
424
|
| `queue-reverify` | An unsettled tie on a blocking kind. Legacy kind; current scoring emits `critic-tie` instead. |
|
|
425
425
|
| `critic-tie` | An unsettled analyser tie on a run that rostered a critic. Dispatch `critic-worker` on those `itemIds` only. The critic's verdict settles the split. Do not re-run the original two. |
|
|
426
|
-
| `
|
|
426
|
+
| `lead-decision` | After the single self-fix: settle eligible, evidenced design judgements through `resolve-dissent` as described in step 8. No worker batch. |
|
|
427
|
+
| `user-decision` | A criticless tie before self-fix, or an unresolved item outside lead authority afterward. Ask immediately through the step 8 approval-blocker protocol. No worker batch. |
|
|
427
428
|
|
|
428
429
|
A referenced **path** that does not exist is still `DISAGREE(b)` / a fact probe, never environment-unverifiable. **Enforced:** `okstra_ctl.plan_items.next_dispatch` / `correction_prompt_text`.
|
|
429
430
|
|
|
@@ -432,7 +433,7 @@ round before any host or provider process starts.
|
|
|
432
433
|
**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.
|
|
433
434
|
|
|
434
435
|
Then run `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. Python appends one immutable round history entry, records each verified item's votes, derives the current projection from the actual assigned roster, and stamps `completedAt` after the preceding verification command succeeds. The file accumulates across rounds; it is never truncated to the latest one. Report assembly later projects the completed nested `planBodyVerification` into the final record.
|
|
435
|
-
7. **Self-fix loop (one rewrite, targeting planner-fixable defects).** After
|
|
436
|
+
7. **Self-fix loop (one rewrite, targeting planner-fixable defects).** After the initial verification, lead may run one report-writer rewrite when a `majority-disagree` item has a majority of `DISAGREE` verdicts at `fixability == planner-fixable`. Re-verify changed items once, preserving verdicts on unchanged content. Then stop automatic self-fix regardless of outcome and follow step 8. The fixed order is initial verification → one planner self-fix → targeted re-verification → lead decision or immediate user confirmation. A second automatic self-fix is rejected by `plan_items_cli._record_self_fixes`; `_validate_self_fix_grouping` and session activity validation detect multiple recorded rewrites. No rewrite is needed when no item qualifies.
|
|
436
437
|
- **Group the targets by cause before instructing (BLOCKING).** Blocked items are usually several derivatives of one defect. Lead partitions this round's targets into cause groups and instructs each group as **"remove this cause"**, naming the derivatives it accounts for. The convergence command owns the persisted group and correction fields; the lead does not edit JSON state.
|
|
437
438
|
- lead instructs report-writer to rewrite the items in each cause group (NOT a full draft regeneration): one `rewrite` entry per item in a corrections ledger, whose `rule` states the cause group's required outcome; procedure in [report-writer](./report-writer.md) §"Corrective report-writer dispatch (ledger required)".
|
|
438
439
|
- 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.
|
|
@@ -446,12 +447,18 @@ round before any host or provider process starts.
|
|
|
446
447
|
- **Loop termination.** The convergence command owns the round count and stop reason; the lead never edits either field in the state file. Both are recorded by `okstra plan-items complete-round`, and its two self-fix flags are not interchangeable. `--self-fix-group` is a claim that this round rewrote something, so it sets `selfFixGroups` and `selfFixRoundsApplied` and **requires** `--self-fix-stop-reason` — there is no default, because the value that used to fill in (`all-resolved`) is the one value step 8 reads as "nothing was left unresolved", which then forbids promoting the items the round did leave unresolved. `--self-fix-stop-reason` on its own is how a round that rewrote nothing records that the loop stops there; it writes `selfFixStopReason` and touches neither `selfFixGroups` nor `selfFixRoundsApplied`, because `validators/validate-run.py` `_validate_self_fix_grouping` requires the highest `selfFixGroups[].round` to equal `selfFixRoundsApplied` — a round number raised without a group has no value that passes. 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:
|
|
447
448
|
- `all-resolved` — no planner-fixable `majority-disagree` item remains. Exit.
|
|
448
449
|
- `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. A round that re-targets only what the previous round left unresolved is this same conclusion reached one dispatch earlier — exit on it under this reason rather than paying for the round that proves it. **Enforced (advisory):** `validators/validate-run.py` `_detect_self_fix_recurrence` warns on that shape and names this stop reason.
|
|
449
|
-
- `max-rounds-reached` — the
|
|
450
|
+
- `max-rounds-reached` — the single automatic rewrite has been used. Exit to step 8. Count distinct `selfFixGroups[].round` values; `selfFixRoundsApplied` is the last verification round number, not the rewrite count. Extra verification batches before the rewrite do not increase the self-fix budget.
|
|
450
451
|
- `cause-group-recurrence` — legacy read-only value for reports produced before activity contract v1. A new activity-contract-v1 run cannot emit it because there is no second automatic self-fix round in which a cause group can recur. **Enforced at the only writer:** `okstra plan-items complete-round --self-fix-stop-reason` accepts `all-resolved` / `no-progress` / `max-rounds-reached` and nothing else (`scripts/okstra_ctl/plan_items_cli.py:261`), and it is what sets `selfFixStopReason`, so the value has no way into a new report. A legacy report that already carries it is still an *exhausted* loop, so step 8 may promote its surviving items: `_SELF_FIX_EXHAUSTED_REASONS` admits this value alongside `no-progress` / `max-rounds-reached`. Refusing it there left such a report with no exit at all — the loop may not run again, and the item may not be promoted either.
|
|
451
452
|
- `not-attempted` — the loop never ran because no item qualified.
|
|
452
453
|
The `no-progress` and `max-rounds-reached` exits are what make the loop terminate; `selfFixMaxRounds` alone is the backstop.
|
|
453
454
|
- 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 (promotion path):** `validators/validate-run.py` `_validate_self_fix_before_clarification` demands an exhausted self-fix budget only of `planner-fixable` majorities, so a `needs-user-input` majority is promotable with no self-fix round, and `_validate_plan_body_clarification_matching` fails it when it reaches no `blocks: approval` row. The classification *value* is authoring guidance per step 8: `scripts/okstra_ctl/approval_decisions.py` checks it against the classification enum and its allowed dispositions only — no validator recomputes it from the votes' `fixability`.
|
|
454
|
-
8.
|
|
455
|
+
8. **Resolve the remaining decisions without another automatic worker batch.** Run `okstra plan-items next-dispatch --state <state> --run-manifest <manifest>` after completing the self-fix verification. It returns `lead-decision` first for eligible items, then `user-decision` for the remainder. It does not schedule another critic or correction batch after the automatic rewrite. Enforcement: `plan_items.next_dispatch` and both state CLI callers.
|
|
456
|
+
|
|
457
|
+
For `lead-decision`, read the evidence and choose within the already agreed scope. Put the decision, why it is within lead authority, and source references in a Markdown file; run `okstra plan-items resolve-dissent --state <state> --item <P-id> --decision-file <file>`. This command records `leadDecision` and a visible `dissentLog` entry without changing votes. Gate status becomes `passed-with-dissent` when no other blocker remains. Enforcement: `plan_items_cli._resolve_dissent`, `validate-run._plan_item_decision_authority`, and `_lead_decision_applies` accept only current, verified, noncritical `b`/`c`/`e` judgements from at least two voting analysers after the one rewrite. A fact claim, `needs-user-input`, or non-result is ineligible. Changed content, scope or votes invalidate the decision's basis hash. The lead should cite the existing requirement or evidence granting authority; uncertainty about that authority goes to the user.
|
|
458
|
+
|
|
459
|
+
For `user-decision`, ask interactively immediately with the concrete choices and their consequences. Requirements changes, scope expansion, risk acceptance, user preferences, unavailable facts, and failed verification stay user-owned. Do not infer approval, replace failure with success, or use self-fix exhaustion as risk acceptance. Record an existing applicable answer instead of asking again. If no answer is available, retain an explicit pending row; elapsed time is not consent. These interaction rules use the approval-blocker protocol in `okstra-lead-contract.md` and the existing approval ledger validation.
|
|
460
|
+
|
|
461
|
+
For every remaining **in-scope execution** blocker or unresolved tie outside lead authority (including `needs-user-input` from the start), add a row to `## 1. Clarification Items` with:
|
|
455
462
|
- do **not** promote an `observed` / `deferred` / `record` item. Those belong in `setAside`. A `Blocks=approval` C row for a frozen or unreached stage is how the clarification list grew while the next stage was already executable. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` uses `_plan_item_gate_class` (after stage scope), not the raw vote class.
|
|
456
463
|
- new `C-<N>` ID (numbering continues from any existing rows)
|
|
457
464
|
- `Statement` summarising the disagreement and the worker breakage `<kind>`
|
|
@@ -474,7 +481,13 @@ round before any host or provider process starts.
|
|
|
474
481
|
- A terminal row preserves its original dissent classification only from the convergence-owned state history. Every `user-decision-required` / `user-decision-evaluated` activity cites the row's `C-NNN` in `clarificationRefs` and affected plan items in `planItemIds`. A resolved decision names only existing `A-NNN` checks. Report assembly validates those links and derives the report backtraces; it does not accept copied IDs from the approval ledger. When an independent coverage-only blocker is corrected, keep the `C-NNN` in the non-blocking Requirement Coverage row's `decisionRefs`. `obsolete` is valid only after current evidence shows that the question or blocker disappeared.
|
|
475
482
|
9. Approval lives in the report record `frontmatter.approved` field — there is no in-body marker line. The user may set it to `true` (via `--approve` or the in-session wizard) when remaining `Blocks=approval` rows are user-proceeded (`accept-risk` / `select` / `answer`) even if the recorded `gateResult` is still `blocked-by-disagreement`. `aborted-non-result` still withholds approval. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan` / `_blocking_gate_survives_user_decision`) and `validators/validate-run.py` `_validate_plan_body_gate_recompute`.
|
|
476
483
|
|
|
477
|
-
##
|
|
484
|
+
## Worker non-result handling in plan-body round (BLOCKING)
|
|
485
|
+
|
|
486
|
+
Mirrors finding convergence ([convergence](./convergence.md) §"Worker failure handling in reverify"). Concretely:
|
|
487
|
+
|
|
488
|
+
- A dispatch that returns terminal non-result MUST NOT be aggregated as `DISAGREE`.
|
|
489
|
+
- If at least one dispatch was issued AND **all** plan-body dispatches return non-result, the Gate result is `aborted-non-result`. Record one `contract-violation` event per non-result dispatch.
|
|
490
|
+
- When the gate is `aborted-non-result`, report-writer MUST keep the frontmatter `approved: false` (publishing `approved: true` under this gate result is a validator failure). A single row is added to `## 1. Clarification Items` with `Statement="plan-body verification could not run — all workers returned non-result"`, `Kind=decision`, `Blocks=approval`, allowing the user to either retry the phase or override by running `--approve` on the resume command (or confirming in the in-session wizard). The row MUST name which dispatches returned no result and what re-running them requires. **Enforced:** `validators/validate-run.py` `_validate_aborted_gate_has_clarification` — `_validate_plan_body_clarification_matching` cannot cover this case because it walks `majority-disagree` items and an aborted round produces none, which is exactly how an aborted run used to reach the user with no stated blocker and stall.
|
|
478
491
|
|
|
479
492
|
**Take the path from the launch prompt, never from this filename (BLOCKING).**
|
|
480
493
|
The run's `## Run Paths` block renders `Plan-body verification state:` with the
|
|
@@ -488,6 +501,27 @@ team-state name, so the round count reads as 0 and
|
|
|
488
501
|
`verification-round-completed count must match automatic plan-body rounds=0`
|
|
489
502
|
fails a run whose rounds all ran.
|
|
490
503
|
|
|
504
|
+
## Conditional reference reading
|
|
505
|
+
|
|
506
|
+
The common procedure ends here. Keep the sections above in context for every
|
|
507
|
+
plan-body round. Read the reference sections below only for the matching operation:
|
|
508
|
+
|
|
509
|
+
| Operation | Additional section to read |
|
|
510
|
+
|---|---|
|
|
511
|
+
| Inspecting a state-shape or projection validation failure | `plan-body-verification-<task-type>-<seq>.json` schema |
|
|
512
|
+
| Investigating a generated prompt rejected by the dispatch gate | Plan-body reverify prompt |
|
|
513
|
+
| Preparing round 2 or later after a rewrite | Re-verification rounds (round 2+) — carry the dissent forward |
|
|
514
|
+
|
|
515
|
+
Use `okstra plan-items prompt` output verbatim for every round. The reference
|
|
516
|
+
prompt is explanatory; it does not replace the generated queue or authorize
|
|
517
|
+
hand-written items. Read a referenced section when an unresolved error requires
|
|
518
|
+
it; do not load every reference pre-emptively. This reading policy is guidance;
|
|
519
|
+
queue validation and dispatch checks continue to enforce the generated input.
|
|
520
|
+
|
|
521
|
+
## Reference material
|
|
522
|
+
|
|
523
|
+
## `plan-body-verification-<task-type>-<seq>.json` schema
|
|
524
|
+
|
|
491
525
|
**Which file is authoritative for what.** Contract v3 keeps both views in one convergence-owned state file before publication:
|
|
492
526
|
|
|
493
527
|
| | records | what a self-fix round does to it |
|
|
@@ -805,11 +839,3 @@ An item with no recorded vote carrying a round number gets no block, and an enve
|
|
|
805
839
|
**Enforced:** `okstra plan-items validate-prepared --state <same state>` re-derives the carry and exits 2 when the prepared envelope's `priorRounds` does not match, alongside the `items` / `dispatchQueue` comparison it already made. A prepared queue that dropped the dissent cannot pass the step-1 validation the dispatch is gated on.
|
|
806
840
|
|
|
807
841
|
The two spellings are different anchors for different artifacts: `**Prior round dissent**` is the block `prompt` puts in the prompt, `**Prior dissent**` is the line the worker puts in its result. `scripts/okstra_ctl/verdict_blocks.py` parses the result line into the verdict block when it is present and leaves it empty when it is not, so an omitted answer line is still silent — the prompt is what is now guaranteed, not the response.
|
|
808
|
-
|
|
809
|
-
## Worker non-result handling in plan-body round (BLOCKING)
|
|
810
|
-
|
|
811
|
-
Mirrors finding convergence ([convergence](./convergence.md) §"Worker failure handling in reverify"). Concretely:
|
|
812
|
-
|
|
813
|
-
- A dispatch that returns terminal non-result MUST NOT be aggregated as `DISAGREE`.
|
|
814
|
-
- If at least one dispatch was issued AND **all** plan-body dispatches return non-result, the Gate result is `aborted-non-result`. Record one `contract-violation` event per non-result dispatch.
|
|
815
|
-
- When the gate is `aborted-non-result`, report-writer MUST keep the frontmatter `approved: false` (publishing `approved: true` under this gate result is a validator failure). A single row is added to `## 1. Clarification Items` with `Statement="plan-body verification could not run — all workers returned non-result"`, `Kind=decision`, `Blocks=approval`, allowing the user to either retry the phase or override by running `--approve` on the resume command (or confirming in the in-session wizard). The row MUST name which dispatches returned no result and what re-running them requires. **Enforced:** `validators/validate-run.py` `_validate_aborted_gate_has_clarification` — `_validate_plan_body_clarification_matching` cannot cover this case because it walks `majority-disagree` items and an aborted round produces none, which is exactly how an aborted run used to reach the user with no stated blocker and stall.
|
|
@@ -100,7 +100,7 @@ A report-writer materialization without `--corrections` whose narrative already
|
|
|
100
100
|
3. Run initial plan-body verification as round 1.
|
|
101
101
|
4. Apply at most one automatic planner self-fix to the narrative. Skip this step when `gating` is `false`.
|
|
102
102
|
5. Run targeted re-verification as round 2 when needed. Skip this step when `gating` is `false`.
|
|
103
|
-
6. Persist the completed `planBodyVerification` value in convergence state.
|
|
103
|
+
6. Persist the completed `planBodyVerification` value in convergence state. Run `okstra plan-items next-dispatch`: after the single automatic self-fix, settle eligible judgements with `resolve-dissent`; ask the user immediately for decisions outside lead authority. Preserve dissent and do not restart the automatic loop. The exact procedure and enforced authority checks are in `prompts/lead/plan-body-verification.md` step 8.
|
|
104
104
|
7. Complete the design-surface detector snapshot: `okstra design-snapshot --narrative <reportNarrativePath> --output <designPreparationPath>`, taking both paths from the run manifest. Nothing else writes that snapshot, and step 8 fails without it — `report_inputs._PLANNING_INPUT_FIELDS` lists `designPreparationPath` as a required planning input.
|
|
105
105
|
8. Run Phase 7 report assembly.
|
|
106
106
|
|
|
@@ -93,7 +93,7 @@ profile document.
|
|
|
93
93
|
- When a response is carried in, reconcile every prior `clarificationItems[]` row against new evidence and update its status to `resolved` or `obsolete` before issuing the next verdict. Schema-v1 compatibility Markdown may additionally render its conditional Section 0; the schema-v2 full reading copy records decisions under `## Clarification and User Decisions`.
|
|
94
94
|
- **Supersession (BLOCKING).** Reconciling the `C-*` row is only half of incorporating an answer. An answer does not merely *add* a decision — it *invalidates* whatever the previous run wrote under the opposite assumption. Before issuing the next decision, walk the prior deliverable prose for every statement the answer makes false and **delete or rewrite it**, then record the retirement. Adding the new decision while leaving the contradicting sentence in place puts two opposite instructions for the same symbol in one document; the implementer must then guess which is live, and the next verification round correctly blocks on it. In `implementation-planning` this record is `implementationPlanning.supersessionLedger[]` — one entry per answered clarification, either `disposition: superseded` (with the retired statement, its replacement, and the sections revised) or `disposition: no-dependent-statement` (with a rationale). A plan built from a selected direction inherits the answers the option-selection record carried before it has any statement to retire, so those carried rows need no entry; the rows this plan itself raised and settled still do. **Enforced:** `validators/validate-run.py` `_validate_supersession_ledger` requires an entry per answered clarification, exempting the ledger's `carriedDecisions[]` ids on a selected-direction plan; whether the claim is *true* is what the §5.5.9 adversarial round tests.
|
|
95
95
|
- Verdict Card data consistency (shared; schema-v1 Markdown keeps the legacy visible card):
|
|
96
|
-
- The Card carries no verdict token — the token lives once, in `finalVerdict.verdictToken`, and every gate reads it there. `verdictCard.direction` byte-matches `finalVerdict.direction`; next-step routing agrees with `recommendedNextSteps[0]`. The full reading copy and human summary are derived from the data fields without repeating both visible sections. **Enforced in part:** the v3.0 schema's `verdictCard` is `additionalProperties: false` with no verdict-token property, so the token cannot be duplicated onto the Card, and `scripts/okstra_ctl/report_narrative.py` `
|
|
96
|
+
- The Card carries no verdict token — the token lives once, in `finalVerdict.verdictToken`, and every gate reads it there. `verdictCard.direction` byte-matches `finalVerdict.direction`; next-step routing agrees with `recommendedNextSteps[0]`. The full reading copy and human summary are derived from the data fields without repeating both visible sections. **Enforced in part:** the v3.0 schema's `verdictCard` is `additionalProperties: false` with no verdict-token property, so the token cannot be duplicated onto the Card, and `scripts/okstra_ctl/report_narrative.py` `writer_owned_schema` applies the finished report's `$defs.Direction` enum to the narrative, rejecting an off-enum `direction` while the writer can still be re-run. The byte-match between the two `direction` fields is not compared by anything — assembly overwrites `nextStep` on both when the plan-body gate passes (`scripts/okstra_ctl/report_assembly.py:590-604`) but leaves `direction` as the writer wrote it.
|
|
97
97
|
- Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
|
|
98
98
|
- **Worker-side item IDs (free-form but unique within the worker).** Every row item in sections 1–5 (and any optional section 6) of an analysis worker's output MUST carry an item ID that is unique within that one worker's result file. The ID convention is the worker's choice — `F-001` / `F-002` per the suggested schema, `1.1` / `1.2` / `1.3` as Codex tends to use, or any other shape — but it MUST appear as the leading column of the row (for table-form items) or as a `[<ID>]` prefix (for bullet/numbered items). Workers that emit findings without IDs make cross-worker reconciliation impossible.
|
|
99
99
|
- **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises consensus, difference, or primary-evidence rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. Each `sourceItems` field MUST list every contributing worker:item pair (e.g. `claude:F-001`, `codex:1.1`, `grok:F-3`, `kimi:2.4`) so an agent can trace the synthesised row to the worker result. Bare worker names are rejected. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.SourceItem` pins each entry to `^[a-z][a-z-]*:[A-Za-z0-9._-]+$`, and `ConsensusRow` / `PrimaryEvidenceRow` require non-empty `sourceItems`.
|
|
@@ -235,7 +235,7 @@ roles:
|
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
An `AGREE` note records the counterexample considered and its exclusion reason. If the judgement needs unavailable external material, record `verification-error`, not `DISAGREE`. **Enforced:** `validators/validate-run.py` `_validate_plan_item_extraction_completeness` compares the exact deterministic set, independently rejecting missing, unexpected, and duplicate plan-item IDs, including `P-Prep-*`.
|
|
238
|
-
- **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead runs a worker peer-review round on the persisted queue. Selected-direction plans begin with `P-Dir-1`; legacy candidate-comparison plans begin with `P-Opt-*`; both continue with the shared execution items. The fixed order remains initial verification → one planner self-fix → targeted re-verification → user
|
|
238
|
+
- **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead runs a worker peer-review round on the persisted queue. Selected-direction plans begin with `P-Dir-1`; legacy candidate-comparison plans begin with `P-Opt-*`; both continue with the shared execution items. The fixed order remains initial verification → one planner self-fix → targeted re-verification → lead decision or immediate user confirmation. Gate recomputation, extraction completeness, approval-context reconciliation, and self-fix limits remain enforced by `validators/validate-run.py`; verdict details and dissent format are owned by `prompts/lead/plan-body-verification.md`.
|
|
239
239
|
- **Approval decision state.** The lead records active and carried decisions through `okstra approval-decision`. Every option carries `disposition`, one `reach`, and optional `scopeEffects`. A resolved decision names existing `A-NNN` checks; report assembly derives `approvalContext`, status, resolution, and reverse links.
|
|
240
240
|
- `open → answered` when the raw user response is recorded
|
|
241
241
|
- `answered → resolved` after the selected disposition is applied, when that work completed
|