okstra 0.199.1 → 0.199.3
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/cli.md +4 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +3 -2
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -2
- package/runtime/prompts/lead/plan-body-verification.md +9 -5
- package/runtime/prompts/lead/report-writer.md +12 -9
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/forbidden-actions.json +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +8 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +10 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +5 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +7 -1
- package/runtime/python/okstra_ctl/blocking_checks.py +1 -1
- package/runtime/python/okstra_ctl/conformance.py +7 -0
- package/runtime/python/okstra_ctl/convergence_store.py +16 -1
- package/runtime/python/okstra_ctl/error_log_core.py +2 -1
- package/runtime/python/okstra_ctl/error_log_write.py +36 -0
- package/runtime/python/okstra_ctl/error_report.py +20 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -4
- package/runtime/python/okstra_ctl/plan_items.py +5 -2
- package/runtime/python/okstra_ctl/plan_items_cli.py +94 -37
- package/runtime/python/okstra_ctl/qa_commands.py +26 -2
- package/runtime/python/okstra_ctl/report_assembly.py +13 -2
- package/runtime/python/okstra_ctl/report_finalize.py +108 -20
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +7 -0
- package/runtime/python/okstra_ctl/run.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +13 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -0
- package/runtime/skills/okstra-inspect/SKILL.md +4 -2
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/validators/forbidden_actions.py +3 -0
- package/runtime/validators/validate-run.py +105 -27
- package/runtime/validators/validate_session_conformance.py +5 -0
|
@@ -246,6 +246,13 @@ class ReportSynthesisPacket:
|
|
|
246
246
|
"that exact order.",
|
|
247
247
|
"`endStateCoverage` must contain exactly one row per original "
|
|
248
248
|
"requirement id; an `addressed` row names its `coveredBy` anchor.",
|
|
249
|
+
"Coverage references use existing identifiers: `stageRefs` contains stage numbers, "
|
|
250
|
+
"`stepRefs` uses `<stage>.<step>` (for example `1.2`), `validationRefs` uses "
|
|
251
|
+
"validation checklist ids, and `fileRefs` uses exact changed file paths without annotations. "
|
|
252
|
+
"Map QA script changes to their requirements too. A future follow-up is not a stage or validation id.",
|
|
253
|
+
"Derive `coverageSummary` from the coverage rows; do not declare exact 100% "
|
|
254
|
+
"or `plan-ready` while requirements are uncovered or file changes are unmapped. "
|
|
255
|
+
"Preserve recorded user decisions when describing deferred work.",
|
|
249
256
|
]
|
|
250
257
|
|
|
251
258
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -394,6 +394,16 @@ def _validate_approved_plan_conformance(path: Path) -> None:
|
|
|
394
394
|
if loaded is None:
|
|
395
395
|
return
|
|
396
396
|
data_path, data = loaded
|
|
397
|
+
from .implementation_direction import stage_validation_executability_errors
|
|
398
|
+
|
|
399
|
+
command_errors = stage_validation_executability_errors(
|
|
400
|
+
data.get("implementationPlanning") or {}
|
|
401
|
+
)
|
|
402
|
+
if command_errors:
|
|
403
|
+
raise PrepareError(
|
|
404
|
+
f"approved plan command preflight failed: {data_path}\n"
|
|
405
|
+
+ "\n".join(command_errors)
|
|
406
|
+
)
|
|
397
407
|
bad = malformed_conformance_stages(data)
|
|
398
408
|
if not bad:
|
|
399
409
|
return
|
|
@@ -391,6 +391,19 @@ def complete_reverify_instruction(
|
|
|
391
391
|
if not body.lstrip().startswith(("## ", "**")):
|
|
392
392
|
body = "## Instructions\n\n" + body
|
|
393
393
|
body = "\n".join(prefix) + "\n\n" + body
|
|
394
|
+
if task_type == "implementation-planning":
|
|
395
|
+
# 저장된 금지 목록은 감사 대조용으로 유지하되 폐기된 작성 의무를 정정한다.
|
|
396
|
+
body = body.rstrip() + (
|
|
397
|
+
"\n\n## Planning conformance ownership\n\n"
|
|
398
|
+
"Planning declares conformance commands and required dependencies; "
|
|
399
|
+
"implementation writes the QA scripts, manifest, and tsconfig. "
|
|
400
|
+
"Do not create those files during planning. Their absence before "
|
|
401
|
+
"implementation is not a planning defect by itself. This phase "
|
|
402
|
+
"ownership supersedes any legacy requirement in the frozen Forbidden "
|
|
403
|
+
"actions block saying this phase MUST write those artifacts. "
|
|
404
|
+
"Verify that the plan assigns their creation to implementation and "
|
|
405
|
+
"provides executable commands and required dependencies.\n"
|
|
406
|
+
)
|
|
394
407
|
if _OUTPUT_CONTRACT_HEADING not in body:
|
|
395
408
|
body = body.rstrip() + "\n\n" + output_contract.strip() + "\n"
|
|
396
409
|
errors = _validate_output_contract_block(body)
|
|
@@ -121,6 +121,16 @@ def is_plan_verify_dispatch_kind(dispatch_kind: str) -> bool:
|
|
|
121
121
|
return _numbered_round(dispatch_kind, PLAN_VERIFY_DISPATCH_KIND_PREFIX) is not None
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def is_plan_critic_verification(
|
|
125
|
+
*, task_type: str, assignment_ref: str, dispatch_kind: str,
|
|
126
|
+
) -> bool:
|
|
127
|
+
return (
|
|
128
|
+
task_type == "implementation-planning"
|
|
129
|
+
and assignment_ref == "critic/scope"
|
|
130
|
+
and is_plan_verify_dispatch_kind(dispatch_kind)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
124
134
|
def verification_dispatch_round(dispatch_kind: str) -> int | None:
|
|
125
135
|
"""검증 kind 가 적는 라운드 번호. 번호 없는 kind(`critic-verify`)는 1,
|
|
126
136
|
검증 kind 가 아니거나 번호가 깨졌으면 None.
|
|
@@ -28,19 +28,21 @@ Single read-side entry point for okstra runtime inspection plus the one status m
|
|
|
28
28
|
|
|
29
29
|
## Step 0: Preflight (shared)
|
|
30
30
|
|
|
31
|
+
Resolve `<host-runtime>` from the launcher's `OKSTRA_RUNTIME_HOST` when present; otherwise use the registered host ID declared by the current harness (`codex` in Codex, `claude-code` in Claude Code). This follows `okstra-run`'s host selection rule. Do not infer the host from worker models, installed executables, or `PATH`. If neither source identifies the host, report that it is unknown instead of substituting Claude Code. Keep the same resolved host when retrying against another project directory.
|
|
32
|
+
|
|
31
33
|
<!-- BEGIN FRAGMENT: bash-invocation-rule -->
|
|
32
34
|
Run one Bash tool call, starting with the literal token `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — a non-literal leading token defeats the `Bash(okstra:*)` permission match):
|
|
33
35
|
<!-- END FRAGMENT: bash-invocation-rule -->
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
|
-
okstra preflight --runtime
|
|
38
|
+
okstra preflight --runtime <host-runtime>
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
The project check only sees the cwd of the Bash call. When the user is asking about a project that is **not** the cwd (a sibling repo, a monorepo subdir, or a project named explicitly in the request), the bare form can report `Okstra preflight: failed` — a false negative, not a missing setup; do not hard-stop on it.
|
|
40
42
|
|
|
41
43
|
Branch on the fixed first line:
|
|
42
44
|
- `Okstra preflight: ready` → carry `Project root` as a literal string; it is the base for every sub-command step below.
|
|
43
|
-
- `Okstra preflight: failed` → before concluding "no setup", ask whether the user pointed at a specific project directory. If they did, re-run targeting it: `okstra preflight --runtime
|
|
45
|
+
- `Okstra preflight: failed` → before concluding "no setup", ask whether the user pointed at a specific project directory. If they did, re-run targeting it: `okstra preflight --runtime <host-runtime> --cwd <that-dir>` (`--cwd` is the sanctioned way to target a project — a leading `cd` would break the permission match). Only if this also fails do you show `Reason` and `Recovery`, then stop.
|
|
44
46
|
|
|
45
47
|
<!-- BEGIN FRAGMENT: preflight-outdated-cli -->
|
|
46
48
|
If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary).
|
|
@@ -475,4 +475,4 @@ Do not read the wizard state file directly. `okstra wizard outcome` exposes any
|
|
|
475
475
|
- Echo each captured answer (`result.echo`) on one short line so the user sees what was registered.
|
|
476
476
|
- Name every file you show the user as a markdown link — `[<what it is>](<path>)`, with the path inside the parentheses. That is the only form the host renders as clickable; a path in backticks is text the user has to copy out. The `report-finalize` result's `reportPaths.markdown` carries the run's report, report record, and team state already in that form. Commands stay in backticks — a link is for a file, not for something to run.
|
|
477
477
|
- Never invent identity; if a `text` prompt returns an empty answer where the wizard rejects it, the user must retry.
|
|
478
|
-
- After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, the end of Step 6 is the end of the run — but in an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish. When the lead (or this skill, after the lead returns) reports
|
|
478
|
+
- After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, the end of Step 6 is the end of the run — but in an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish. When `report-finalize` returns `recovery.mode: same-run`, continue the authorized corrections in this run and execute `recovery.resumeCommand` before closeout; preserve approvals and model choices without reopening the wizard. The command and owner issues are supplied by `report_finalize._finalize_recovery`. When the lead (or this skill, after the lead returns) reports a successfully finalized run over, close with the user's next action — one command they can run now. A prohibition is not a next action. Take the pointer from the `report-finalize` result's top-level `nextRecommendedPhase` (`phase`, `status`, `rationale`; also on stderr as `next phase status:` / `next phase:` / `next phase rationale:`) — do not re-derive it from the report, and treat a `nextRecommendedPhaseError` as "pointer unreadable", said in one line before the `validate-run` branch. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead. After `implementation-planning`, open `blocks: approval` rows → `/okstra-user-response`. A recorded `accept-risk` / `select` / `answer` is not an open blocker. No open approval blocker → `/okstra-run` → `implementation` or `--approve` (do not start another planning run; do not say `/okstra-inspect`). For every other task type, quote the pointer's `rationale` in every branch — that sentence is the report's own reason and it is what the user asked to be analysed. Pointer `status: ready` → `/okstra-run` for that phase; `status: terminal` → say the task is finished, name any follow-up tasks this run registered, and do not say `/okstra-inspect`; `status: blocked` → issue the command the `rationale` calls for (`/okstra-user-response` for the `C-NNN` ids, `/okstra-run` for the phase it names); `validate-run` failed with `recovery.mode: phase-reentry` → name the cause and use `nextCommand` for the recorded earlier phase; otherwise `/okstra-inspect status`.
|
|
@@ -108,11 +108,14 @@ def verifier_mutation_hits(command_log: str) -> list[tuple[str, str]]:
|
|
|
108
108
|
로그는 자유 문자열이라 줄 단위로 본다 — 어느 명령이 문제인지 그대로 보고해야
|
|
109
109
|
작성자가 고칠 자리를 안다. heredoc 본문은 기록되는 데이터이므로 걷어낸다.
|
|
110
110
|
"""
|
|
111
|
+
from okstra_ctl.qa_commands import find_unfrozen_installs
|
|
112
|
+
|
|
111
113
|
hits: list[tuple[str, str]] = []
|
|
112
114
|
for raw in strip_heredoc_bodies(command_log or "").splitlines():
|
|
113
115
|
line = raw.strip()
|
|
114
116
|
if not line or line.startswith("#"):
|
|
115
117
|
continue
|
|
118
|
+
hits.extend((label, line) for label in find_unfrozen_installs(line))
|
|
116
119
|
for label, pattern in (*_VERIFIER_MUTATION, *_VERIFIER_SOURCE_MUTATION):
|
|
117
120
|
if pattern.search(line):
|
|
118
121
|
hits.append((label, line))
|
|
@@ -902,11 +902,22 @@ def update_workflow_metadata(
|
|
|
902
902
|
if current_phase:
|
|
903
903
|
phase_states[current_phase] = current_phase_state
|
|
904
904
|
last_completed_phase = workflow.get("lastCompletedPhase", "")
|
|
905
|
+
recovery = next_phase.project(report_data or {})
|
|
906
|
+
recovery_phase = recovery["phase"]
|
|
907
|
+
can_backtrack = (
|
|
908
|
+
current_phase in PHASE_SEQUENCE
|
|
909
|
+
and recovery_phase in PHASE_SEQUENCE
|
|
910
|
+
and PHASE_SEQUENCE.index(recovery_phase)
|
|
911
|
+
< PHASE_SEQUENCE.index(current_phase)
|
|
912
|
+
)
|
|
905
913
|
next_recommended_phase = next_phase.make(
|
|
914
|
+
phase=recovery_phase if can_backtrack else "",
|
|
906
915
|
status=next_phase.STATUS_BLOCKED,
|
|
907
916
|
rationale=_blocked_rationale(
|
|
908
|
-
current_phase,
|
|
909
|
-
|
|
917
|
+
recovery_phase if can_backtrack else current_phase,
|
|
918
|
+
recovery["rationale"]
|
|
919
|
+
if can_backtrack
|
|
920
|
+
else inherited_pointer["rationale"],
|
|
910
921
|
failures,
|
|
911
922
|
run_manifest_path=str(run_manifest.get("runManifestPath") or ""),
|
|
912
923
|
),
|
|
@@ -2197,6 +2208,12 @@ def _validate_planning_conformance_declared(
|
|
|
2197
2208
|
if not isinstance(ip, dict):
|
|
2198
2209
|
return
|
|
2199
2210
|
_planning_conformance_declarations(ip.get("stages"), failures)
|
|
2211
|
+
if ip.get("planningContract") != "selected-direction":
|
|
2212
|
+
from okstra_ctl.implementation_direction import (
|
|
2213
|
+
stage_validation_executability_errors,
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
failures.extend(stage_validation_executability_errors(ip))
|
|
2200
2217
|
for conflict in exempt_stage_surface_conflicts(data, surface_patterns):
|
|
2201
2218
|
failures.append(
|
|
2202
2219
|
"conformance gate BLOCKING: stage "
|
|
@@ -3688,31 +3705,30 @@ def _single_vote_block_survives(item: dict, kinds: set[str]) -> bool:
|
|
|
3688
3705
|
|
|
3689
3706
|
|
|
3690
3707
|
def _critic_non_error_verdicts(item: dict) -> list[dict]:
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
]
|
|
3708
|
+
"""현재 기록된 비판 검토자의 최신 유효 판정."""
|
|
3709
|
+
rows = [row for row in item.get("verdicts", []) if isinstance(row, dict)]
|
|
3710
|
+
critic = [row for row in rows
|
|
3711
|
+
if is_critic_worker(row.get("worker", ""))
|
|
3712
|
+
and str(row.get("verdict", "")).upper() in {"AGREE", "SUPPLEMENT", "DISAGREE"}]
|
|
3713
|
+
latest = max((row.get("round", 1) for row in critic), default=0)
|
|
3714
|
+
return [row for row in critic if row.get("round", 1) == latest]
|
|
3699
3715
|
|
|
3700
3716
|
|
|
3701
|
-
def
|
|
3702
|
-
"""
|
|
3703
|
-
if not (len(disagree) == len(agree) and disagree):
|
|
3704
|
-
return None
|
|
3717
|
+
def _critic_gate_class(item: dict) -> str | None:
|
|
3718
|
+
"""비판 검토자의 교정 권한은 분석자의 표수나 동수 여부에 의존하지 않는다."""
|
|
3705
3719
|
critic = _critic_non_error_verdicts(item)
|
|
3706
3720
|
if not critic:
|
|
3707
|
-
return
|
|
3708
|
-
if
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3721
|
+
return None
|
|
3722
|
+
critic_dissent = [row for row in critic if str(row.get("verdict", "")).upper() == "DISAGREE"]
|
|
3723
|
+
if critic_dissent:
|
|
3724
|
+
if str(item.get("id", "")).upper().startswith("P-RB"):
|
|
3725
|
+
return "has-dissent"
|
|
3726
|
+
return "majority-disagree" if any(
|
|
3727
|
+
str(row.get("breakageKind", "")).lower() not in _ADVISORY_ONLY_KINDS
|
|
3728
|
+
for row in critic_dissent
|
|
3729
|
+
) else "has-dissent"
|
|
3730
|
+
dissent = any(str(row.get("verdict", "")).upper() == "DISAGREE" for row in item.get("verdicts", []))
|
|
3731
|
+
return "has-dissent" if dissent else "full-consensus"
|
|
3716
3732
|
|
|
3717
3733
|
|
|
3718
3734
|
def _classify_plan_item_gate(item: dict) -> str:
|
|
@@ -3726,6 +3742,9 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3726
3742
|
reproduction. An analyser 1-1 is ``needs-reverify`` until ``critic-worker``
|
|
3727
3743
|
settles it.
|
|
3728
3744
|
"""
|
|
3745
|
+
corrected = _critic_gate_class(item)
|
|
3746
|
+
if corrected is not None:
|
|
3747
|
+
return corrected
|
|
3729
3748
|
tokens = [
|
|
3730
3749
|
(
|
|
3731
3750
|
str(v.get("verdict") or "").strip().upper(),
|
|
@@ -3783,9 +3802,8 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3783
3802
|
# made the gate stricter than a healthy roster would.
|
|
3784
3803
|
if len(non_error) >= 2 and len(blocking_disagree) > len(agree):
|
|
3785
3804
|
return "majority-disagree"
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
return settled
|
|
3805
|
+
if len(blocking_disagree) == len(agree) and len(non_error) >= 2:
|
|
3806
|
+
return "needs-reverify"
|
|
3789
3807
|
if (
|
|
3790
3808
|
len(non_error) >= 2
|
|
3791
3809
|
and blocking_disagree
|
|
@@ -9363,13 +9381,66 @@ def _data_schema_failures(data: dict) -> list[str]:
|
|
|
9363
9381
|
]
|
|
9364
9382
|
|
|
9365
9383
|
|
|
9384
|
+
def run_preflight(report_path: Path, run_manifest_path: Path) -> int:
|
|
9385
|
+
"""번역·표시·상태 갱신 전에 정본의 구조와 적합성만 검사한다."""
|
|
9386
|
+
try:
|
|
9387
|
+
data = load_json(_data_path_for(report_path))
|
|
9388
|
+
manifest = load_json(run_manifest_path)
|
|
9389
|
+
if not isinstance(data, dict) or not isinstance(manifest, dict):
|
|
9390
|
+
raise TypeError("preflight requires report and run-manifest JSON objects")
|
|
9391
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
9392
|
+
print(json.dumps({"ok": False, "failures": [str(exc)]}))
|
|
9393
|
+
return 2
|
|
9394
|
+
failures = _data_schema_failures(data)
|
|
9395
|
+
task_type = manifest.get("taskType")
|
|
9396
|
+
warnings: list[str] = []
|
|
9397
|
+
if task_type in ("implementation", "final-verification"):
|
|
9398
|
+
project_root = Path(
|
|
9399
|
+
str(manifest.get("projectRoot") or run_manifest_path.parent)
|
|
9400
|
+
)
|
|
9401
|
+
warnings = _validate_conformance(
|
|
9402
|
+
report_path,
|
|
9403
|
+
failures,
|
|
9404
|
+
surface_patterns=_project_surface_patterns(project_root),
|
|
9405
|
+
approved_plan_path=_approved_plan_path_from_run_inputs(
|
|
9406
|
+
run_manifest_path, failures
|
|
9407
|
+
),
|
|
9408
|
+
)
|
|
9409
|
+
elif task_type == "implementation-planning":
|
|
9410
|
+
from okstra_ctl.report_assembly import selected_direction_plan_errors
|
|
9411
|
+
from okstra_ctl.implementation_direction import (
|
|
9412
|
+
stage_validation_executability_errors,
|
|
9413
|
+
)
|
|
9414
|
+
|
|
9415
|
+
failures.extend(
|
|
9416
|
+
stage_validation_executability_errors(
|
|
9417
|
+
data.get("implementationPlanning") or {}
|
|
9418
|
+
)
|
|
9419
|
+
)
|
|
9420
|
+
failures.extend(selected_direction_plan_errors(
|
|
9421
|
+
data, Path(str(manifest.get("projectRoot") or run_manifest_path.parent)), manifest
|
|
9422
|
+
))
|
|
9423
|
+
_validate_planning_conformance_declared(report_path, failures)
|
|
9424
|
+
print(
|
|
9425
|
+
json.dumps(
|
|
9426
|
+
{
|
|
9427
|
+
"ok": not failures,
|
|
9428
|
+
"failures": list(dict.fromkeys(failures)),
|
|
9429
|
+
"warnings": warnings,
|
|
9430
|
+
},
|
|
9431
|
+
ensure_ascii=False,
|
|
9432
|
+
)
|
|
9433
|
+
)
|
|
9434
|
+
return 2 if failures else 0
|
|
9435
|
+
|
|
9436
|
+
|
|
9366
9437
|
def main() -> int:
|
|
9367
9438
|
parser = argparse.ArgumentParser(
|
|
9368
9439
|
description="Validate okstra run contract artifacts."
|
|
9369
9440
|
)
|
|
9370
9441
|
parser.add_argument(
|
|
9371
9442
|
"--section",
|
|
9372
|
-
choices=(SECTION_FULL, SECTION_PLAN_BODY),
|
|
9443
|
+
choices=(SECTION_FULL, SECTION_PLAN_BODY, "preflight"),
|
|
9373
9444
|
default=SECTION_FULL,
|
|
9374
9445
|
help=(
|
|
9375
9446
|
"Which contract surface to validate. `full` (default) validates the "
|
|
@@ -9415,6 +9486,13 @@ def main() -> int:
|
|
|
9415
9486
|
)
|
|
9416
9487
|
args = parser.parse_args()
|
|
9417
9488
|
|
|
9489
|
+
if args.section == "preflight":
|
|
9490
|
+
if not args.report or not args.run_manifest:
|
|
9491
|
+
parser.error("--section preflight requires --report and --run-manifest")
|
|
9492
|
+
return run_preflight(
|
|
9493
|
+
Path(args.report).resolve(), Path(args.run_manifest).resolve()
|
|
9494
|
+
)
|
|
9495
|
+
|
|
9418
9496
|
if args.section == SECTION_PLAN_BODY:
|
|
9419
9497
|
if args.narrative and args.state and not args.report:
|
|
9420
9498
|
return run_plan_body_inputs(
|
|
@@ -561,6 +561,11 @@ def _progress_line_from_event(event) -> tuple[str, str, str] | None:
|
|
|
561
561
|
message = details.get("message")
|
|
562
562
|
tail = f" {message}" if isinstance(message, str) and message else ""
|
|
563
563
|
line = f"PROGRESS: {phase}{tail}"
|
|
564
|
+
worker = details.get("worker")
|
|
565
|
+
if isinstance(worker, str) and worker.strip():
|
|
566
|
+
from okstra_ctl.lead_progress import render_progress_line
|
|
567
|
+
|
|
568
|
+
line = render_progress_line(phase, [("worker", worker), ("detail", line)])
|
|
564
569
|
return (event.timestamp, phase, line)
|
|
565
570
|
|
|
566
571
|
|