okstra 0.223.2 → 0.225.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/dist/commands/execute/render-bundle.mjs +7 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/docs/cli.md +10 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +1 -0
- package/runtime/bin/lib/okstra/usage.sh +4 -1
- package/runtime/bin/okstra-provider-exec.py +32 -16
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/coding-preflight/clean-code.md +8 -1
- package/runtime/prompts/duties/code-reviewer.json +5 -3
- package/runtime/prompts/wizard/prompts.ko.json +10 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +3 -1
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/providers/zai/adapter.py +5 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/results.py +25 -0
- package/runtime/python/okstra_ctl/code_review_coverage.py +213 -0
- package/runtime/python/okstra_ctl/code_review_target.py +31 -2
- package/runtime/python/okstra_ctl/coverage_census.py +2 -2
- package/runtime/python/okstra_ctl/dispatch_core.py +15 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +6 -0
- package/runtime/python/okstra_ctl/incremental_carry.py +10 -16
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-diff-review.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +164 -33
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +27 -3
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +33 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +1 -0
- package/runtime/python/okstra_ctl/phases/implementation_planning/round_completion.py +177 -0
- package/runtime/python/okstra_ctl/plan_item_evidence.py +237 -0
- package/runtime/python/okstra_ctl/plan_items.py +12 -9
- package/runtime/python/okstra_ctl/plan_review_context.py +82 -0
- package/runtime/python/okstra_ctl/render.py +2 -0
- package/runtime/python/okstra_ctl/run.py +23 -2
- package/runtime/python/okstra_ctl/team.py +12 -18
- package/runtime/python/okstra_ctl/verdict_blocks.py +17 -1
- package/runtime/python/okstra_ctl/wizard/__init__.py +1 -0
- package/runtime/python/okstra_ctl/wizard/confirmation.py +2 -0
- package/runtime/python/okstra_ctl/wizard/engine.py +26 -5
- package/runtime/python/okstra_ctl/wizard/ids.py +3 -0
- package/runtime/python/okstra_ctl/wizard/registry.py +13 -0
- package/runtime/python/okstra_ctl/wizard/render.py +1 -0
- package/runtime/python/okstra_ctl/wizard/state.py +8 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +99 -16
- package/runtime/python/okstra_ctl/wizard/steps_options.py +52 -1
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +14 -0
- package/runtime/python/okstra_ctl/worker_dispatch.py +9 -0
- package/runtime/python/okstra_ctl/worker_request.py +12 -1
- package/runtime/schemas/execution-manifest-v2.schema.json +1 -0
- package/runtime/schemas/final-report-v2.0.schema.json +3 -1
- package/runtime/schemas/final-report-v3.0.schema.json +3 -1
- package/runtime/skills/_fragments/bash-invocation-rule.md +1 -1
- package/runtime/skills/_fragments/host-question-interface.md +1 -0
- package/runtime/skills/_fragments/host-runtime-selection.md +1 -0
- package/runtime/skills/_fragments/wizard-bash-invocation-rule.md +1 -1
- package/runtime/skills/_fragments/wizard-preflight.md +1 -1
- package/runtime/skills/_fragments/wizard-render-bundle.md +4 -4
- package/runtime/skills/okstra-brief-gen/SKILL.md +45 -47
- package/runtime/skills/okstra-brief-gen/references/reporter-confirmations.md +3 -3
- package/runtime/skills/okstra-brief-gen/references/tracker-recursion.md +1 -1
- package/runtime/skills/okstra-chat/SKILL.md +1 -1
- package/runtime/skills/okstra-code-review/SKILL.md +26 -15
- package/runtime/skills/okstra-code-review/references/census-rules.md +38 -2
- package/runtime/skills/okstra-code-review/references/review-calibration.md +39 -15
- package/runtime/skills/okstra-container-build/SKILL.md +6 -2
- package/runtime/skills/okstra-discussion/SKILL.md +6 -6
- package/runtime/skills/okstra-inspect/SKILL.md +5 -3
- package/runtime/skills/okstra-manager/SKILL.md +8 -4
- package/runtime/skills/okstra-memory/SKILL.md +1 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +10 -6
- package/runtime/skills/okstra-rollup/SKILL.md +6 -2
- package/runtime/skills/okstra-run/SKILL.md +8 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +13 -6
- package/runtime/skills/okstra-setup/SKILL.md +8 -4
- package/runtime/skills/okstra-setup/references/project-config.md +3 -3
- package/runtime/skills/okstra-usage/SKILL.md +5 -1
- package/runtime/skills/okstra-user-response/SKILL.md +7 -3
|
@@ -11,8 +11,10 @@ from . import plan_body
|
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import copy
|
|
14
|
+
from dataclasses import dataclass
|
|
14
15
|
import json
|
|
15
16
|
import re
|
|
17
|
+
import subprocess
|
|
16
18
|
from collections.abc import Mapping, Sequence
|
|
17
19
|
from datetime import datetime, timezone
|
|
18
20
|
from pathlib import Path
|
|
@@ -25,6 +27,7 @@ from okstra_ctl.convergence import (
|
|
|
25
27
|
validated_run_authority,
|
|
26
28
|
)
|
|
27
29
|
from okstra_ctl.convergence_store import write_json_atomic
|
|
30
|
+
from okstra_ctl.paths import RUN_VALIDATOR_RELATIVE, find_asset_root
|
|
28
31
|
from okstra_ctl.incremental_carry import sync_prepared_dispatch_queue
|
|
29
32
|
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
|
|
30
33
|
from okstra_ctl.report_finalize import task_manifest_path
|
|
@@ -54,6 +57,14 @@ from okstra_ctl.plan_items import (
|
|
|
54
57
|
with_response_format,
|
|
55
58
|
)
|
|
56
59
|
from okstra_ctl.paths import RunRef
|
|
60
|
+
from okstra_ctl.plan_item_evidence import (
|
|
61
|
+
capture_review_inputs,
|
|
62
|
+
dependency_scopes,
|
|
63
|
+
reusable_item_ids,
|
|
64
|
+
review_fingerprint,
|
|
65
|
+
review_files_unchanged,
|
|
66
|
+
)
|
|
67
|
+
from okstra_ctl.plan_review_context import render_review_context
|
|
57
68
|
from okstra_ctl.stage_ledger import build_stage_ledger
|
|
58
69
|
from okstra_ctl.claim_reproduction import NOT_RUNNABLE, reproduce
|
|
59
70
|
from okstra_ctl.final_report_schema import load_schema_version
|
|
@@ -118,13 +129,39 @@ def _queue_for(
|
|
|
118
129
|
run_manifest: Path | None,
|
|
119
130
|
previous_hashes: Mapping[str, str] | None = None,
|
|
120
131
|
tied_ids: Sequence[str] | None = None,
|
|
132
|
+
previous_rows: Sequence[Mapping[str, Any]] = (),
|
|
133
|
+
review_inputs: Mapping[str, Any] | None = None,
|
|
121
134
|
) -> list[str]:
|
|
122
135
|
ledger = _merged_ledger(planning, run_manifest)
|
|
123
136
|
if tied_ids is not None:
|
|
124
137
|
return tie_vote_item_ids(items, ledger, tied_ids)
|
|
138
|
+
inputs = review_inputs or _review_inputs(items, planning, run_manifest, previous_rows)
|
|
139
|
+
reusable = reusable_item_ids(previous_rows, inputs)
|
|
140
|
+
candidates = set(dispatch_item_ids(items, ledger))
|
|
125
141
|
if previous_hashes:
|
|
126
|
-
|
|
127
|
-
return
|
|
142
|
+
candidates.update(reverify_item_ids(items, previous_hashes, ledger))
|
|
143
|
+
return [item["id"] for item in items if item["id"] in candidates - reusable]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _review_inputs(
|
|
147
|
+
items: list[dict[str, Any]], planning: Mapping[str, Any],
|
|
148
|
+
run_manifest: Path | None, rows: Sequence[Mapping[str, Any]],
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
if run_manifest is None:
|
|
151
|
+
return {}
|
|
152
|
+
authority = validated_run_authority(run_manifest)
|
|
153
|
+
return capture_review_inputs(items, planning, authority.payload, authority.project_root, rows)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _checked_review_inputs(run_manifest: Path | None) -> dict[str, Any]:
|
|
157
|
+
root = _probe_project_root(run_manifest)
|
|
158
|
+
if root is None:
|
|
159
|
+
return {}
|
|
160
|
+
path = _prepared_items_path(run_manifest)
|
|
161
|
+
inputs = _load_json_object(path).get("reviewInputs", {}) if path.is_file() else {}
|
|
162
|
+
if not review_files_unchanged(inputs, root):
|
|
163
|
+
raise PlanItemContractError("review evidence changed during dispatch; prepare and verify again")
|
|
164
|
+
return inputs
|
|
128
165
|
|
|
129
166
|
|
|
130
167
|
def _queue_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
|
@@ -133,7 +170,9 @@ def _queue_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
133
170
|
if state is None:
|
|
134
171
|
raise PlanItemContractError("--tie-vote requires --state")
|
|
135
172
|
return {"tied_ids": _tied_ids(state)}
|
|
136
|
-
|
|
173
|
+
state = getattr(args, "state", None)
|
|
174
|
+
rows = _state_plan_body_items(_load_json_object(state), state) if state and state.is_file() else []
|
|
175
|
+
return {"previous_hashes": _previous_hashes(state), "previous_rows": rows}
|
|
137
176
|
|
|
138
177
|
|
|
139
178
|
def _tied_ids(state_path: Path) -> list[str]:
|
|
@@ -170,8 +209,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
170
209
|
prepare.add_argument(
|
|
171
210
|
"--state",
|
|
172
211
|
type=Path,
|
|
173
|
-
help="previous plan-body state;
|
|
174
|
-
"
|
|
212
|
+
help="previous plan-body state; only matching body and complete "
|
|
213
|
+
"evidence fingerprints shrink the self-fix reverify queue, and "
|
|
175
214
|
"each queued item's recorded votes and selfFixNote are carried "
|
|
176
215
|
"into the prompt as its `**Prior round dissent**` block",
|
|
177
216
|
)
|
|
@@ -251,8 +290,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
251
290
|
"--prior-state",
|
|
252
291
|
type=Path,
|
|
253
292
|
help="the previous run's plan-body-verification-<task-type>-<seq>.json. "
|
|
254
|
-
"A newly seeded item
|
|
255
|
-
"
|
|
293
|
+
"A newly seeded item with matching body and complete evidence "
|
|
294
|
+
"fingerprints inherits its verdicts and is tagged "
|
|
256
295
|
"carriedForwardFromSeq, so round 1 does not re-judge text nobody "
|
|
257
296
|
"changed. Requires --state, refuses a state file belonging to "
|
|
258
297
|
"another task, and never carries on a matching id alone",
|
|
@@ -474,12 +513,21 @@ def _prepare(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
474
513
|
+ "; correct the narrative in this run, then retry plan-items prepare"
|
|
475
514
|
)
|
|
476
515
|
envelope = _envelope(source)
|
|
516
|
+
kwargs = _queue_kwargs(args)
|
|
517
|
+
envelope["reviewInputs"] = _review_inputs(
|
|
518
|
+
envelope["items"], _planning(source), args.run_manifest,
|
|
519
|
+
kwargs.get("previous_rows", []),
|
|
520
|
+
)
|
|
477
521
|
envelope["dispatchQueue"] = _queue_for(
|
|
478
522
|
envelope["items"],
|
|
479
523
|
_planning(source),
|
|
480
524
|
args.run_manifest,
|
|
481
|
-
**
|
|
525
|
+
**kwargs,
|
|
526
|
+
review_inputs=envelope["reviewInputs"],
|
|
482
527
|
)
|
|
528
|
+
envelope["reusedItems"] = sorted(reusable_item_ids(
|
|
529
|
+
kwargs.get("previous_rows", []), envelope["reviewInputs"],
|
|
530
|
+
))
|
|
483
531
|
if getattr(args, "tie_vote", False):
|
|
484
532
|
envelope["dispatchKind"] = "critic-tie"
|
|
485
533
|
envelope["tieSplits"] = _tie_splits_from_state(
|
|
@@ -998,6 +1046,7 @@ def _render_inputs(run_manifest: Path) -> str:
|
|
|
998
1046
|
|
|
999
1047
|
|
|
1000
1048
|
def _prompt(args: argparse.Namespace) -> str:
|
|
1049
|
+
inputs = _checked_review_inputs(args.run_manifest)
|
|
1001
1050
|
envelope = _load_json_object(
|
|
1002
1051
|
_prepared_items_path(args.run_manifest, require_regular=True)
|
|
1003
1052
|
)
|
|
@@ -1021,6 +1070,11 @@ def _prompt(args: argparse.Namespace) -> str:
|
|
|
1021
1070
|
line("Item count", len(items)),
|
|
1022
1071
|
_render_inputs(args.run_manifest),
|
|
1023
1072
|
]
|
|
1073
|
+
authority = validated_run_authority(args.run_manifest)
|
|
1074
|
+
rows.append(render_review_context(
|
|
1075
|
+
[item["id"] for item in items], inputs, authority.payload, authority.project_root,
|
|
1076
|
+
))
|
|
1077
|
+
_checked_review_inputs(args.run_manifest)
|
|
1024
1078
|
for index, item in enumerate(items, 1):
|
|
1025
1079
|
if not isinstance(item, Mapping):
|
|
1026
1080
|
continue
|
|
@@ -1075,6 +1129,12 @@ def _validate_prepared(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1075
1129
|
)
|
|
1076
1130
|
if actual.get("dispatchQueue") != expected_queue:
|
|
1077
1131
|
raise PlanItemContractError("prepared dispatch queue does not match")
|
|
1132
|
+
current_inputs = _review_inputs(
|
|
1133
|
+
expected["items"], _planning(source), args.run_manifest,
|
|
1134
|
+
_queue_kwargs(args).get("previous_rows", []),
|
|
1135
|
+
)
|
|
1136
|
+
if actual.get("reviewInputs") != current_inputs:
|
|
1137
|
+
raise PlanItemContractError("prepared review evidence changed; prepare again")
|
|
1078
1138
|
if not getattr(args, "tie_vote", False):
|
|
1079
1139
|
# 라운드 2+ 의 반대 의견 반입은 여기서만 검사된다. 봉투에서 빠지면
|
|
1080
1140
|
# 프롬프트에도 빠지고, 반대한 워커는 자기 판정을 그대로 다시 적는다.
|
|
@@ -1089,6 +1149,69 @@ def _validate_prepared(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1089
1149
|
return {"ok": True, "operation": "validate-prepared", "path": str(path)}
|
|
1090
1150
|
|
|
1091
1151
|
|
|
1152
|
+
@dataclass(frozen=True)
|
|
1153
|
+
class PreparedPlanReview:
|
|
1154
|
+
manifest: Path
|
|
1155
|
+
state: Path
|
|
1156
|
+
items: Path
|
|
1157
|
+
narrative: Path
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def validate_current_review(run_manifest: Path) -> PreparedPlanReview:
|
|
1161
|
+
authority = validated_run_authority(run_manifest)
|
|
1162
|
+
state = canonical_run_state_artifact(
|
|
1163
|
+
authority, manifest_field="planBodyVerificationPath",
|
|
1164
|
+
prefix="plan-body-verification", label="plan body verification path",
|
|
1165
|
+
)
|
|
1166
|
+
name = authority.payload.get("reportNarrativePath")
|
|
1167
|
+
if not isinstance(name, str) or not name:
|
|
1168
|
+
raise PlanItemContractError("reportNarrativePath is missing")
|
|
1169
|
+
narrative = (authority.project_root / name).resolve(strict=True)
|
|
1170
|
+
if not narrative.is_relative_to(authority.run_dir):
|
|
1171
|
+
raise PlanItemContractError("plan narrative must stay inside the current run")
|
|
1172
|
+
items_path = _prepared_items_path(run_manifest, require_regular=True)
|
|
1173
|
+
prepared = _load_json_object(items_path)
|
|
1174
|
+
source = _plan_source(argparse.Namespace(data=None, narrative=narrative))
|
|
1175
|
+
items = _envelope(source)["items"]
|
|
1176
|
+
if prepared.get("items") != items:
|
|
1177
|
+
raise PlanItemContractError("prepared plan items do not match the current narrative")
|
|
1178
|
+
inputs = _checked_review_inputs(run_manifest)
|
|
1179
|
+
current = _review_inputs(items, _planning(source), run_manifest, [])
|
|
1180
|
+
if inputs.get("context") != current.get("context"):
|
|
1181
|
+
raise PlanItemContractError("prepared review context changed; prepare and verify again")
|
|
1182
|
+
return PreparedPlanReview(run_manifest, state, items_path, narrative)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def finish_verification_round(
|
|
1186
|
+
review: PreparedPlanReview, results: Sequence[str], round_number: int,
|
|
1187
|
+
) -> dict[str, Any]:
|
|
1188
|
+
common = ["--state", str(review.state), "--run-manifest", str(review.manifest),
|
|
1189
|
+
"--items", str(review.items), "--round", str(round_number)]
|
|
1190
|
+
apply_args = ["apply-verdicts", *common]
|
|
1191
|
+
for result in results:
|
|
1192
|
+
apply_args.extend(["--result", result])
|
|
1193
|
+
if results and all(is_critic_worker(value.split("=", 1)[0]) for value in results):
|
|
1194
|
+
apply_args.append("--append")
|
|
1195
|
+
_apply_verdicts(_parser().parse_args(apply_args))
|
|
1196
|
+
assets = find_asset_root(RUN_VALIDATOR_RELATIVE)
|
|
1197
|
+
if assets is None:
|
|
1198
|
+
raise PlanItemContractError("plan-body validator is unavailable")
|
|
1199
|
+
verified = subprocess.run(
|
|
1200
|
+
[sys.executable, str(assets.joinpath(*RUN_VALIDATOR_RELATIVE)),
|
|
1201
|
+
"--section", "plan-body", "--narrative", str(review.narrative),
|
|
1202
|
+
"--state", str(review.state)],
|
|
1203
|
+
capture_output=True, text=True, check=False, timeout=60,
|
|
1204
|
+
)
|
|
1205
|
+
if verified.returncode:
|
|
1206
|
+
raise PlanItemContractError(
|
|
1207
|
+
f"plan-verify failed: {verified.stdout.strip()} {verified.stderr.strip()}"
|
|
1208
|
+
)
|
|
1209
|
+
validation = json.loads(verified.stdout)
|
|
1210
|
+
validate_current_review(review.manifest)
|
|
1211
|
+
completed = _complete_round(_parser().parse_args(["complete-round", *common]))
|
|
1212
|
+
return {**completed, "verification": validation}
|
|
1213
|
+
|
|
1214
|
+
|
|
1092
1215
|
def _split_result_arg(raw: str) -> tuple[str, Path]:
|
|
1093
1216
|
worker, separator, path = raw.partition("=")
|
|
1094
1217
|
if not separator or not worker.strip() or not path.strip():
|
|
@@ -1134,6 +1257,7 @@ def _verdict_row(worker: str, block: VerdictBlock) -> dict[str, Any]:
|
|
|
1134
1257
|
("fixability", block.fixability),
|
|
1135
1258
|
("note", block.note),
|
|
1136
1259
|
("explanation", block.explanation),
|
|
1260
|
+
("dependencies", block.dependencies),
|
|
1137
1261
|
):
|
|
1138
1262
|
if value:
|
|
1139
1263
|
row[key] = value
|
|
@@ -1367,24 +1491,6 @@ def _carry_prior_run_verdicts(
|
|
|
1367
1491
|
added: list[dict[str, Any]],
|
|
1368
1492
|
hashes: Mapping[str, str],
|
|
1369
1493
|
) -> _PriorRunCarry | None:
|
|
1370
|
-
"""직전 run 이 같은 본문에 내린 판정을 새로 시드된 행에 옮긴다.
|
|
1371
|
-
|
|
1372
|
-
실패한 계획 run 을 다시 돌면 판정이 하나도 넘어오지 않았다. 이월은 답변된
|
|
1373
|
-
clarification 이 있는 경로에만 있고(`incremental_scope.decide_scope` 는
|
|
1374
|
-
답변된 `C-NNN` 이 없으면 `full` 을 되돌려 `incremental_carry` 를 도달 불가로
|
|
1375
|
-
만든다), 그래서 아무도 고치지 않은 문장이 매 재실행마다 다시 채점됐다.
|
|
1376
|
-
|
|
1377
|
-
행 모양은 `incremental_carry._stamp_carried` 와 같다 — `verdicts` 를 그대로
|
|
1378
|
-
옮기고, `carriedForwardFromSeq` 에 직전 seq 를 찍고, `verifiedContentHash` 를
|
|
1379
|
-
이번 추출 해시로 맞춘다. 그 해시가 시드의 `dispatchQueue` 계산에 그대로
|
|
1380
|
-
들어가므로(`_queue_for` → `reverify_item_ids`), 이월된 행은 라운드 1 큐에서
|
|
1381
|
-
빠진다. 큐를 따로 줄이지 않는 이유가 이것이다: incremental 경로가
|
|
1382
|
-
`incremental_carry._recompute_dispatch_queue` 에서 쓰는 것과 같은 장치다.
|
|
1383
|
-
|
|
1384
|
-
id 만 같은 행은 절대 이월하지 않는다. `P-*` id 는 위치에서 나오므로 계획이
|
|
1385
|
-
한 줄만 밀려도 같은 번호가 다른 문장을 가리킨다. 해시가 같아야 그 문장이
|
|
1386
|
-
같은 문장이다.
|
|
1387
|
-
"""
|
|
1388
1494
|
prior = getattr(args, "prior_state", None)
|
|
1389
1495
|
if prior is None:
|
|
1390
1496
|
return None
|
|
@@ -1420,9 +1526,17 @@ def _carry_prior_run_verdicts(
|
|
|
1420
1526
|
current_hash = hashes.get(item_id)
|
|
1421
1527
|
if not current_hash or previous.get("verifiedContentHash") != current_hash:
|
|
1422
1528
|
continue
|
|
1529
|
+
manifest = getattr(args, "run_manifest", None)
|
|
1530
|
+
inputs = (
|
|
1531
|
+
_load_json_object(_prepared_items_path(manifest)).get("reviewInputs", {})
|
|
1532
|
+
if manifest is not None else {}
|
|
1533
|
+
)
|
|
1534
|
+
if item_id not in reusable_item_ids([previous], inputs):
|
|
1535
|
+
continue
|
|
1423
1536
|
row["verdicts"] = copy.deepcopy(verdicts)
|
|
1424
1537
|
row["carriedForwardFromSeq"] = prev_seq
|
|
1425
1538
|
row["verifiedContentHash"] = current_hash
|
|
1539
|
+
row["verifiedReviewHash"] = previous["verifiedReviewHash"]
|
|
1426
1540
|
count += 1
|
|
1427
1541
|
return _PriorRunCarry(prev_seq, count)
|
|
1428
1542
|
|
|
@@ -1526,6 +1640,7 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1526
1640
|
_planning(source),
|
|
1527
1641
|
getattr(args, "run_manifest", None),
|
|
1528
1642
|
previous,
|
|
1643
|
+
previous_rows=recorded,
|
|
1529
1644
|
)
|
|
1530
1645
|
verification["gating"] = (
|
|
1531
1646
|
verification.get("gating") is True
|
|
@@ -1609,8 +1724,21 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1609
1724
|
f"gate is re-derived from that table, so a verdict with nowhere to "
|
|
1610
1725
|
f"land would be scored as if it were never cast"
|
|
1611
1726
|
)
|
|
1727
|
+
review_inputs = _checked_review_inputs(getattr(args, "run_manifest", None))
|
|
1612
1728
|
if args.round_number < 1:
|
|
1613
1729
|
raise PlanItemContractError("--round must be 1 or greater")
|
|
1730
|
+
_write_round_verdicts(args, data, recorded, rows, review_inputs)
|
|
1731
|
+
if requires_plan_repair(verification):
|
|
1732
|
+
verification["gating"] = True
|
|
1733
|
+
_validate_advisory_round(verification, args.round_number)
|
|
1734
|
+
write_json_atomic(target, data)
|
|
1735
|
+
return {"ok": True, "operation": "apply-verdicts", "path": str(target)}
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def _write_round_verdicts(
|
|
1739
|
+
args: argparse.Namespace, data: Mapping[str, Any], recorded: list[dict[str, Any]],
|
|
1740
|
+
rows: Mapping[str, list[dict[str, Any]]], review_inputs: Mapping[str, Any],
|
|
1741
|
+
) -> None:
|
|
1614
1742
|
project_root = _probe_project_root(getattr(args, "run_manifest", None))
|
|
1615
1743
|
append = bool(getattr(args, "append", False))
|
|
1616
1744
|
replaces_critic = append and any(
|
|
@@ -1627,18 +1755,20 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1627
1755
|
rows,
|
|
1628
1756
|
data.get("roundHistory"),
|
|
1629
1757
|
args.round_number,
|
|
1630
|
-
|
|
1631
|
-
discard_open_rounds=discard_open_rounds,
|
|
1758
|
+
args.state or args.data,
|
|
1759
|
+
discard_open_rounds=bool(getattr(args, "discard_open_rounds", False)),
|
|
1632
1760
|
)
|
|
1633
1761
|
writer = _append_item_verdicts if append else _replace_item_verdicts
|
|
1634
1762
|
for item in recorded:
|
|
1635
1763
|
if isinstance(item, Mapping) and item.get("id") in rows:
|
|
1636
1764
|
writer(item, rows[item["id"]], args.round_number, project_root)
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1765
|
+
review_inputs = {**review_inputs, "dependencies": dependency_scopes(recorded)}
|
|
1766
|
+
for item in recorded:
|
|
1767
|
+
if isinstance(item, Mapping) and item.get("id") in rows:
|
|
1768
|
+
item.pop("verifiedReviewHash", None)
|
|
1769
|
+
fingerprint = review_fingerprint(item, review_inputs)
|
|
1770
|
+
if fingerprint is not None:
|
|
1771
|
+
item["verifiedReviewHash"] = fingerprint
|
|
1642
1772
|
|
|
1643
1773
|
|
|
1644
1774
|
def _stamped_verdicts(
|
|
@@ -2110,6 +2240,7 @@ def _reject_unbacked_self_fix_stop(
|
|
|
2110
2240
|
|
|
2111
2241
|
|
|
2112
2242
|
def _complete_round(args: argparse.Namespace) -> dict[str, Any]:
|
|
2243
|
+
_checked_review_inputs(args.run_manifest)
|
|
2113
2244
|
if args.round_number < 1:
|
|
2114
2245
|
raise PlanItemContractError("--round must be 1 or greater")
|
|
2115
2246
|
data, current, audit, history = _round_inputs(args)
|
|
@@ -413,18 +413,22 @@ round before any host or provider process starts.
|
|
|
413
413
|
|
|
414
414
|
For contract 3.0, `prepare` checks the selected-direction draft with the same semantic validator as publication before writing the queue. Correct all reported references, coverage rows and planned commands through the writer correction procedure, then retry in this run. **Enforced:** `report_assembly.validate_plan_draft` and `plan_items_cli._prepare`; `tests/report/test_report_assembly.py` checks that invalid coverage creates no prepared queue.
|
|
415
415
|
|
|
416
|
-
1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared
|
|
416
|
+
1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared. The runtime reuses a verdict only when its body, complete declared evidence closure, shared analysis packet, plan metadata, reviewer assignments and review contract have the same fingerprint. A missing or uncertain dependency declaration, unresolved reference, non-result or executable fact probe stays in the re-verification queue. Detailed verdicts and their original rounds remain unchanged for reused items. File dependencies cover whole files; step items also include preceding steps; plan-wide and preparation items include all plan items. Python resolves the one convergence-owned state path from that run identity. Dispatch only after exact-match validation succeeds. `observed` / `deferred` stages remain omitted. An empty queue opens no worker dispatch and creates no verification round. **Enforced:** `implementation_planning.authoring._queue_for`, `plan_item_evidence.review_fingerprint`, `_validate_prepared` and `tests/run/test_plan_items.py`. An incremental carry without evidence snapshots keeps active items queued until prepare can validate their dependencies.
|
|
417
417
|
|
|
418
418
|
**Then seed the landing table (BLOCKING):** `okstra plan-items seed --narrative <report-writer-narrative.md> --state <plan-body-verification.json> --run-manifest <run-manifest.json>`. `apply-verdicts` in step 8 refuses a verdict whose item has no `planBodyVerification.planItems[]` row. The report writer never owns that state, so the deterministic seed is the only creator of its rows. The seed is idempotent by id and never touches existing verdicts, so it is safe to re-run between rounds and after a self-fix re-extraction. It does refresh `contentHash` from the current extract. Skipping it makes step 8 fail with `plan-body state has no row for [...]`.
|
|
419
419
|
|
|
420
|
-
**On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item
|
|
420
|
+
**On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item inherits verdicts only when its body and verified evidence fingerprint match the prepared snapshot for the same task. Reused rows carry `carriedForwardFromSeq`; their full verdicts are preserved. Legacy body-only records re-enter verification. A matching id alone never carries: `P-*` ids are positional. The flag requires `--state` and refuses another task's state. **Enforced:** `implementation_planning.authoring._carry_prior_run_verdicts` and `plan_item_evidence.reusable_item_ids`.
|
|
421
421
|
|
|
422
422
|
**`--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`.
|
|
423
423
|
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.
|
|
424
424
|
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.
|
|
425
425
|
**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):** `scripts/okstra_ctl/phases/implementation_planning/plan_body.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.
|
|
426
426
|
|
|
427
|
-
4. After all dispatches return, lead
|
|
427
|
+
4. After all dispatches return, first read `planVerification` in the `worker-dispatch`, `team await --json`, or `agent-prompt link-result --json` response. When its `status` is `completed`, the runtime has already parsed all accepted verifier results, run the plan-body contract validator, and recorded the round. Use its `gateResult`, `verification` (including advisories), and `nextDispatch`; skip the separate collect/apply/plan-verify/complete/next-dispatch calls and continue with the decisions and self-fix procedure below. `pending` means the complete prepared verifier roster has not returned. `failed` stops automatic completion and returns the error; preserve the result files and repair the named defect. `needs-lead`, or a response without this field, uses the manual procedure below. In particular, self-fix re-verification retains the lead-owned cause groups and completion metadata. **Enforced:** `phases/implementation_planning/round_completion.py` and the automatic-round and execution-boundary tests in `tests/run/test_plan_items.py`.
|
|
428
|
+
|
|
429
|
+
A critic correction sharing the analysers' round also returns `needs-lead`; retain the original round queue and use the manual `--append` procedure. The automatic-round tests in `tests/run/test_plan_items.py` enforce this boundary.
|
|
430
|
+
|
|
431
|
+
In the manual procedure, lead aggregates verdicts per `P-*` item across workers and classifies each:
|
|
428
432
|
|
|
429
433
|
**Every in-scope item carries at least one verdict row (BLOCKING).** Aggregation covers the dispatch queue, not deferred or observed stages. An in-scope item left with an empty `verdicts[]` is not a weak signal the gate can discount — it classifies `all-non-result`, states as `needs-reverify`, and folds into `passed-with-dissent` next to items two verifiers actually agreed on, so a plan item nobody judged reads as a passing one. This is the shape a self-fix round produces when the planner adds an in-scope item and the targeted round-N queue never picks it up. A worker that returned nothing is a `verification-error` row (step 3), not a missing row; if an in-scope item was never dispatched, dispatch it before scoring the round. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_round_recorded_verdicts` fails any run whose `roundCount` ≥ 1 leaves an in-scope item with no verdict row.
|
|
430
434
|
|
|
@@ -693,6 +697,23 @@ and a `- Plan narrative:` line from its `reportNarrativePath`. The worker's
|
|
|
693
697
|
are what let a verifier read the requirement a selected-direction `P-Req-*` row
|
|
694
698
|
names by `originalRequirementId`. The lead does not add them by hand.
|
|
695
699
|
|
|
700
|
+
When a complete input snapshot is available, the renderer also provides inline
|
|
701
|
+
review context. Read that context and the assigned payloads first. It preserves
|
|
702
|
+
the packet except its navigation index and generated analysis Coverage Census,
|
|
703
|
+
and carries plan metadata plus supporting items as lossless JSON. Supporting
|
|
704
|
+
items are narrowed only when the previous dependency closure is complete and
|
|
705
|
+
every body in that closure is unchanged; otherwise all other plan items are
|
|
706
|
+
included. A changed body, missing declaration, unresolved reference or uncertain
|
|
707
|
+
cross-item claim requires the full context or the original Inputs paths.
|
|
708
|
+
Read those paths when the supplied context does not resolve a question, and
|
|
709
|
+
record the additional dependencies. Omission never proves absence.
|
|
710
|
+
Packets containing finding-like headings are quoted as source data, preserving
|
|
711
|
+
their text without adding foreign items to the plan-verification queue.
|
|
712
|
+
|
|
713
|
+
Enforcement: `render_review_context` in `okstra_ctl.plan_review_context` selects
|
|
714
|
+
the context; `test_plan_review_context_preserves_facts_and_falls_back_for_unknown_dependencies`
|
|
715
|
+
in `tests/run/test_plan_items.py` checks retained facts and conservative fallback.
|
|
716
|
+
|
|
696
717
|
Carrying the packet does not license re-analysis. It is there so the verifier
|
|
697
718
|
can resolve a plan item back to the requirement it claims to satisfy; the
|
|
698
719
|
posture in §"Adversarial plan-body posture" still applies, and this round does
|
|
@@ -847,6 +868,9 @@ never recorded, and the round is scored on the items that remain.
|
|
|
847
868
|
**Probe** (only when Claim is fact): <one line of JSON, e.g. {"kind": "path-absent", "path": "src/missing.ts"}>
|
|
848
869
|
**Note**: <the falsification candidate considered and why it is excluded; required even for AGREE>
|
|
849
870
|
**Explanation**: <2-3 sentences>
|
|
871
|
+
**Dependencies**: {"complete": false, "itemIds": [], "paths": []}
|
|
872
|
+
|
|
873
|
+
Keep the detailed Note and Explanation. Declare all direct and transitive plan-item and whole-file evidence dependencies. Set `complete` to true only when that boundary is known. Include source files behind references. External or dynamic evidence, unlisted narrative dependencies and unresolved references keep `complete` false. This declaration controls reuse eligibility, not the verdict itself. Missing and malformed declarations are treated as unknown by `verdict_blocks._block` and `plan_item_evidence.review_fingerprint`.
|
|
850
874
|
|
|
851
875
|
### P-Opt-2
|
|
852
876
|
...
|
|
@@ -68,6 +68,7 @@ from okstra_ctl.design_surfaces import DesignSurfaceError # noqa: E402
|
|
|
68
68
|
|
|
69
69
|
from okstra_ctl.plan_items import ( # noqa: E402
|
|
70
70
|
content_hash,
|
|
71
|
+
PlanItemContractError,
|
|
71
72
|
expected_plan_item_ids,
|
|
72
73
|
extract_plan_items,
|
|
73
74
|
)
|
|
@@ -2543,6 +2544,7 @@ def validate_plan_body_section(
|
|
|
2543
2544
|
_validate_round_recorded_verdicts(data, failures)
|
|
2544
2545
|
_validate_verdicts_match_current_subjects(data, failures)
|
|
2545
2546
|
_validate_verdict_rounds_outlive_self_fix(data, failures)
|
|
2547
|
+
_validate_review_evidence(data, report_path, failures)
|
|
2546
2548
|
_validate_unresolved_tie_was_reverified(data, failures)
|
|
2547
2549
|
_validate_advisory_plan_body_gating(data, failures)
|
|
2548
2550
|
_validate_plan_item_extraction_completeness(data, failures)
|
|
@@ -2553,6 +2555,37 @@ def validate_plan_body_section(
|
|
|
2553
2555
|
return [*_detect_self_fix_recurrence(pbv), *_detect_uniform_verifier(pbv)]
|
|
2554
2556
|
|
|
2555
2557
|
|
|
2558
|
+
def _validate_review_evidence(data: dict, report_path: Path, failures: list[str]) -> None:
|
|
2559
|
+
from okstra_ctl.convergence import validated_run_authority, ConvergenceContractError
|
|
2560
|
+
from okstra_ctl.plan_item_evidence import capture_review_inputs, review_fingerprint
|
|
2561
|
+
|
|
2562
|
+
planning = data.get("implementationPlanning") or {}
|
|
2563
|
+
pbv = planning.get("planBodyVerification") or {}
|
|
2564
|
+
rows = pbv.get("planItems") or []
|
|
2565
|
+
if not any(row.get("verifiedReviewHash") for row in rows):
|
|
2566
|
+
return
|
|
2567
|
+
sequences = _manifest_seq_for_report(report_path, "manifests")
|
|
2568
|
+
if len(sequences) != 1:
|
|
2569
|
+
failures.append("plan review evidence has no unambiguous run manifest")
|
|
2570
|
+
return
|
|
2571
|
+
path = report_path.parent.parent / "manifests" / (
|
|
2572
|
+
f"run-manifest-implementation-planning-{next(iter(sequences))}.json"
|
|
2573
|
+
)
|
|
2574
|
+
try:
|
|
2575
|
+
authority = validated_run_authority(path)
|
|
2576
|
+
inputs = capture_review_inputs(
|
|
2577
|
+
extract_plan_items(planning), planning, authority.payload, authority.project_root, rows,
|
|
2578
|
+
)
|
|
2579
|
+
except (ConvergenceContractError, PlanItemContractError) as exc:
|
|
2580
|
+
failures.append(f"cannot validate plan review evidence: {exc}")
|
|
2581
|
+
return
|
|
2582
|
+
stale = [row["id"] for row in rows if _item_stage_scope_bucket(row, pbv) == "in-scope"
|
|
2583
|
+
and row.get("verifiedReviewHash")
|
|
2584
|
+
and row["verifiedReviewHash"] != review_fingerprint(row, inputs)]
|
|
2585
|
+
if stale:
|
|
2586
|
+
failures.append(f"plan review evidence changed for {stale}; prepare and re-verify those items")
|
|
2587
|
+
|
|
2588
|
+
|
|
2556
2589
|
def _gate_summary_item(
|
|
2557
2590
|
item: dict,
|
|
2558
2591
|
pbv: dict,
|
|
@@ -292,6 +292,7 @@ For a new `implementation-planning` run, the fixed order is initial verification
|
|
|
292
292
|
|
|
293
293
|
An `AGREE` response records the considered counterexample and exclusion reason in its note; unverified external material is `verification-error`, not `DISAGREE`.
|
|
294
294
|
2. Dispatch a single plan-body reverify round to every analyser worker in the run's roster. `Report writer worker` is NOT a participant in this round.
|
|
295
|
+
Read `planVerification` in the dispatch, await, or native result-link response. `status=completed` means the runtime has already applied the accepted results, checked the plan-body contract, and completed the round. Use its gate, advisories, and next dispatch and skip steps 3–4. `pending` waits for the remaining verifiers; `failed` stops for the returned defect; `needs-lead` retains the manual path, including self-fix cause-group recording. Enforcement: `phases/implementation_planning/round_completion.py` and automatic-round integration tests in `tests/run/test_plan_items.py`.
|
|
295
296
|
3. Record each verifier Markdown result through `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md> --round <N>`. Python validates every submitted `P-*` identifier against the current convergence state and overwrites only that round's verdicts. Then resolve the gate result to one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`.
|
|
296
297
|
4. After `okstra plan-verify` succeeds, run `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. Python reads the current worker assignments, atomically appends the convergence-owned history, and updates its nested final projection. This state is the only plan-verification input report assembly reads.
|
|
297
298
|
5. Record every *in-scope execution* `majority-disagree` decision through `okstra approval-decision`; record its plan and clarification links only on activities. Do not promote `observed` / `deferred` / `record` items, and do not append `clarificationItems[]` directly.
|