okstra 0.193.0 → 0.194.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/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +1 -1
- package/runtime/python/okstra_ctl/plan_items_cli.py +48 -2
- package/runtime/python/okstra_ctl/session_transcript.py +33 -3
- package/runtime/validators/validate-run.py +6 -1
package/docs/cli.md
CHANGED
|
@@ -801,7 +801,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
801
801
|
| `okstra plan-items derivations --data <data.json> --response <user-response sidecar> [--clarification C-NNN]` | List the plan statements an answered clarification may have falsified. Extracts the symbols, paths, and ids the answer names (backticked spans plus `R-001` / `DEV-10174`-style ids) and reports every string in the plan body that mentions one, as a JSON pointer plus excerpt. Advisory: it says where a decision's subject is mentioned, never which mentions are now wrong — the supersession rule (`prompts/profiles/_common-contract.md`) requires the author to enumerate before editing, and this supplies the enumeration |
|
|
802
802
|
| `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. |
|
|
803
803
|
| `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. |
|
|
804
|
-
| `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. 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). |
|
|
804
|
+
| `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. 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. |
|
|
805
805
|
| `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`. |
|
|
806
806
|
| `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. |
|
|
807
807
|
| `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. |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -427,7 +427,7 @@ round before any host or provider process starts.
|
|
|
427
427
|
- **Drop plan items whose element the round deleted.** A self-fix rewrite may remove a plan element (a validation check, a rollback row). `P-*` ids are positional, so a deletion shifts every later row and silently re-points surviving verdicts at their neighbours — and a verdict recorded against a removed element keeps blocking a gate while being unfindable in the plan, so reading the plan never reveals the cause. After each round, re-extract plan items with `okstra plan-items extract` and re-verify any item whose `subject` no longer matches; never carry the old vote forward across a shift. **Enforced:** `validators/validate-run.py` `_validate_verdicts_match_current_subjects` (re-pointing) and `_validate_plan_item_extraction_completeness` (dangling ids).
|
|
428
428
|
- **Classify each cause group before instructing it (BLOCKING).** A group is either an *authoring* defect — the plan says something wrong, incomplete, or self-contradictory, which self-fix owns — or a *citation* defect, where the plan points at an analysis artifact incorrectly. Only the first is self-fix work. For the second the finding already exists and already went through convergence, so the fix is to re-cite the converged artifact; instructing report-writer to re-derive the fact means the author reads the source material and produces a **finding that never went through convergence**, which the plan then carries as if it had. That is the role boundary the lead contract draws ("keep analysis, execution, verification, and report authoring responsibilities distinct; return defects to the role that owns them"), and report-writer is authoring-only by its own contract. `P-Req-*` items with breakage kind `f` are where this goes wrong most often: the question is usually whether a coverage row points correctly at something already measured, not whether the measurement is right. State the classification in the group's instruction so the author knows which of the two it is being asked to do.
|
|
429
429
|
- **A verdict older than the last self-fix is not a verdict unless the item's content is unchanged (BLOCKING).** A verdict cast in round 1 judged the text before the only automatic rewrite. Once that rewrite runs, a changed item's judgement is about a plan that no longer exists. `--round <N>` on `apply-verdicts` stamps each row and copies `contentHash` onto `verifiedContentHash`. `validators/validate-run.py` `_validate_verdict_rounds_outlive_self_fix` fails an in-scope item whose verdict round is at or before `selfFixRoundsApplied` **and** whose `contentHash` does not match `verifiedContentHash`. Matching hashes keep the prior verdict — that is what avoids a sweep round over unchanged stages. Deferred and observed items are out of the gate and do not need a post-self-fix verdict. **Enforced:** `_validate_verdict_rounds_outlive_self_fix`.
|
|
430
|
-
- Lead re-runs plan-body verification, then records each worker Markdown result through `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker>=<result.md> --round <N>`. Score the result with `okstra plan-verify --narrative <report-writer-narrative.md> --state <plan-body-verification.json>`, then call `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. These commands fail on an assigned item the worker left unanswered, on a verdict for an item outside the queue, and on a duplicate worker result. `apply-verdicts` without `--append` replaces every recorded row of the queued items, so it also refuses — before writing — when a row belongs to a round that `complete-round` never closed, naming the round to close first; the votes of a closed round live in `planItems[].rounds`. Skipping `complete-round` between rounds and applying the next one lost 19 items' round-1 votes (2026-09-09). `--discard-open-rounds` is only for re-applying the lost rounds from their result files in order. **Enforced:** `okstra_ctl.plan_items_cli._reject_uncompleted_round_loss`.
|
|
430
|
+
- Lead re-runs plan-body verification, then records each worker Markdown result through `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker>=<result.md> --round <N>`. Score the result with `okstra plan-verify --narrative <report-writer-narrative.md> --state <plan-body-verification.json>`, then call `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. These commands fail on an assigned item the worker left unanswered, on a verdict for an item outside the queue, and on a duplicate worker result. `apply-verdicts` without `--append` replaces every recorded row of the queued items, so it also refuses — before writing — when a row belongs to a round that `complete-round` never closed, naming the round to close first; the votes of a closed round live in `planItems[].rounds`. Skipping `complete-round` between rounds and applying the next one lost 19 items' round-1 votes (2026-09-09). `--discard-open-rounds` is only for re-applying the lost rounds from their result files in order: it also restores `dispatchQueue` to the items those result files answer, because the persisted queue is the latest round's and a verdict for an earlier, wider queue is otherwise refused as outside it (round 1 = 26 items, critic tie = 8, round 3 = 19 on that same run — only the 19 survived). Re-apply each lost round with the flag, `complete-round` it, then the next. **Enforced:** `okstra_ctl.plan_items_cli._reject_uncompleted_round_loss` and `_restore_queue_from_results`.
|
|
431
431
|
- For a self-fix, record the correction through the typed convergence command rather than writing `selfFixNote` or `selfFixGroups` JSON. A resolved item does not create a clarification.
|
|
432
432
|
- **Each round is a worker batch.** Before dispatching round N ≥ 2, reclaim the previous round's completed verifiers exactly as at any other batch boundary ([okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle") and emit `PROGRESS: phase-batch-cleanup panes=<n>`, then announce the round with `PROGRESS: phase-5.5.9-plan-verify round=<N> items=<count>`. Saying a round will "reuse" the previous verifiers and then dispatching under fresh names leaves every prior round holding its panes — five rounds of that is what exhausts the pane budget and blocks the next dispatch. **Enforced:** `validators/validate_session_conformance.py` `_check_plan_verify_cleanup_checkpoints` requires both lines once the state file records two or more rounds.
|
|
433
433
|
- **Round completion.** A round is complete only after `okstra plan-verify` exits 0 and `okstra plan-items complete-round` succeeds. A round left with a non-zero exit carries its defect into the next round's inputs. Exit 0 with a non-empty `advisories[]` is a complete round: those findings are recorded, not round-blocking (step 5, §"`failures[]` carries only the blocking findings"). Report assembly and rendering occur only after the convergence state is terminal. **Enforced:** `validators/validate-run.py` `_validate_plan_body_state_rounds` requires one stored round per round number and a corresponding item vote.
|
|
@@ -263,7 +263,9 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
263
263
|
"--discard-open-rounds", action="store_true",
|
|
264
264
|
help="replace even the rows of a round that complete-round never "
|
|
265
265
|
"closed — the recovery path when those rows are being re-applied "
|
|
266
|
-
"from their result files in order; the discarded rows are listed"
|
|
266
|
+
"from their result files in order; the discarded rows are listed, "
|
|
267
|
+
"and dispatchQueue is restored to the items those result files "
|
|
268
|
+
"answer, since the persisted queue belongs to the latest round",
|
|
267
269
|
)
|
|
268
270
|
complete = commands.add_parser(
|
|
269
271
|
"complete-round",
|
|
@@ -1367,6 +1369,9 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1367
1369
|
data["planBodyVerification"] if args.state is not None
|
|
1368
1370
|
else _planning(data)["planBodyVerification"]
|
|
1369
1371
|
)
|
|
1372
|
+
discard_open_rounds = bool(getattr(args, "discard_open_rounds", False))
|
|
1373
|
+
if discard_open_rounds and args.state is not None and args.result:
|
|
1374
|
+
_restore_queue_from_results(verification, args.result, recorded)
|
|
1370
1375
|
queue = verification.get("dispatchQueue") if isinstance(verification, Mapping) else None
|
|
1371
1376
|
assigned = (
|
|
1372
1377
|
{item_id for item_id in queue if isinstance(item_id, str)}
|
|
@@ -1399,7 +1404,7 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1399
1404
|
if not append:
|
|
1400
1405
|
_reject_uncompleted_round_loss(
|
|
1401
1406
|
recorded, rows, data.get("roundHistory"), args.round_number, target,
|
|
1402
|
-
discard_open_rounds=
|
|
1407
|
+
discard_open_rounds=discard_open_rounds,
|
|
1403
1408
|
)
|
|
1404
1409
|
writer = _append_item_verdicts if append else _replace_item_verdicts
|
|
1405
1410
|
for item in recorded:
|
|
@@ -1459,6 +1464,47 @@ def _append_item_verdicts(
|
|
|
1459
1464
|
_remember_verified_hash(item)
|
|
1460
1465
|
|
|
1461
1466
|
|
|
1467
|
+
def _restore_queue_from_results(
|
|
1468
|
+
verification: Any,
|
|
1469
|
+
raw_results: list[str],
|
|
1470
|
+
recorded: Sequence[Mapping[str, Any]],
|
|
1471
|
+
) -> None:
|
|
1472
|
+
"""재적용하는 라운드의 큐를 그 라운드의 결과 파일이 답한 항목 집합으로 되돌린다.
|
|
1473
|
+
|
|
1474
|
+
`dispatchQueue` 는 값이 하나뿐이라 라운드마다 덮인다 — 라운드 1 이 26개,
|
|
1475
|
+
critic 동수 라운드가 8개, 라운드 3 이 19개였던 run 에서 상태에 남는 것은
|
|
1476
|
+
19개뿐이다. `apply-verdicts` 와 `complete-round` 는 둘 다 그 큐로 스코프하고,
|
|
1477
|
+
`_queue_for` 는 이미 두 번 전진한 해시로부터의 순수 계산이라 지난 큐를
|
|
1478
|
+
되살릴 명령이 없었다. 그래서 `--discard-open-rounds` 로 라운드 1 을 다시
|
|
1479
|
+
적용하면 큐 밖 id 라며 거절됐고, 복구는 상태 파일을 손으로 고쳐야만 됐다
|
|
1480
|
+
(실측 2026-09-09, `fontsninja-v3-site` dev-10627 planning 002).
|
|
1481
|
+
|
|
1482
|
+
결과 파일의 `### P-*` 블록 집합이 곧 그 라운드의 큐다 — 워커는 배정된 항목
|
|
1483
|
+
전부에 답해야 통과하고(`_worker_blocks`), 큐 밖 항목에는 답할 수 없었다.
|
|
1484
|
+
순서는 계획 항목 순서를 따른다. 계획에 없는 id 는 큐에 넣지 않으므로 뒤의
|
|
1485
|
+
`_worker_blocks` 가 그 id 를 이름하며 거절한다.
|
|
1486
|
+
"""
|
|
1487
|
+
if not isinstance(verification, dict):
|
|
1488
|
+
return
|
|
1489
|
+
answered: set[str] = set()
|
|
1490
|
+
for raw in raw_results:
|
|
1491
|
+
_worker, path = _split_result_arg(raw)
|
|
1492
|
+
try:
|
|
1493
|
+
answered |= set(parse_verdict_blocks(path.read_text(encoding="utf-8")))
|
|
1494
|
+
except (OSError, UnicodeError) as exc:
|
|
1495
|
+
raise PlanItemContractError(f"cannot read result {path}: {exc}") from exc
|
|
1496
|
+
restored = [
|
|
1497
|
+
str(item["id"]) for item in recorded
|
|
1498
|
+
if isinstance(item, Mapping) and item.get("id") in answered
|
|
1499
|
+
]
|
|
1500
|
+
verification["dispatchQueue"] = restored
|
|
1501
|
+
print(
|
|
1502
|
+
f"apply-verdicts: dispatchQueue restored from the result files "
|
|
1503
|
+
f"({len(restored)} items)",
|
|
1504
|
+
file=sys.stderr,
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
|
|
1462
1508
|
def _reject_uncompleted_round_loss(
|
|
1463
1509
|
recorded: Sequence[Mapping[str, Any]],
|
|
1464
1510
|
rows: Mapping[str, Any],
|
|
@@ -5,12 +5,23 @@
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
8
10
|
from datetime import datetime
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Callable
|
|
11
13
|
|
|
12
14
|
OKSTRA = "okstra"
|
|
13
15
|
_SPEAKER_WIDTH = 14
|
|
16
|
+
_RESET = "\x1b[0m"
|
|
17
|
+
_MUTED = "\x1b[90m"
|
|
18
|
+
_LIVE_COLORS = (
|
|
19
|
+
("→ ", "\x1b[36m"),
|
|
20
|
+
(" ← ok", "\x1b[32m"),
|
|
21
|
+
(" ← error", "\x1b[31m"),
|
|
22
|
+
("!! PERMISSION DENIED", "\x1b[1;31m"),
|
|
23
|
+
(" ← done", "\x1b[33m"),
|
|
24
|
+
)
|
|
14
25
|
|
|
15
26
|
# 파일 사본이 담는 워커 진행 줄의 상한. 진행은 워커 출력의 부피가 몰리는
|
|
16
27
|
# 자리다 — 파일 하나만 읽는 디스패치도 킬로바이트 단위 도구 에코를 남기고,
|
|
@@ -39,6 +50,12 @@ class SessionTranscript:
|
|
|
39
50
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
51
|
self._file = path.open("w", encoding="utf-8")
|
|
41
52
|
self._live = live
|
|
53
|
+
self._color = (
|
|
54
|
+
live
|
|
55
|
+
and sys.stdout.isatty()
|
|
56
|
+
and not os.environ.get("NO_COLOR")
|
|
57
|
+
and os.environ.get("TERM") != "dumb"
|
|
58
|
+
)
|
|
42
59
|
self._clock = clock
|
|
43
60
|
self._archived = 0
|
|
44
61
|
self._elided = 0
|
|
@@ -51,7 +68,7 @@ class SessionTranscript:
|
|
|
51
68
|
잘린 결론은 사후 분석 전체를 잃는다.
|
|
52
69
|
"""
|
|
53
70
|
row = self._row(speaker, line)
|
|
54
|
-
self._show(row)
|
|
71
|
+
self._show(row, line)
|
|
55
72
|
self._keep(row, capped=capped)
|
|
56
73
|
|
|
57
74
|
def write_event(
|
|
@@ -67,7 +84,7 @@ class SessionTranscript:
|
|
|
67
84
|
기록에서는 46KB 를 읽은 호출과 잘린 호출이 같은 줄로 보인다.
|
|
68
85
|
"""
|
|
69
86
|
for line in screen:
|
|
70
|
-
self._show(self._row(speaker, line))
|
|
87
|
+
self._show(self._row(speaker, line), line)
|
|
71
88
|
for line in archive:
|
|
72
89
|
self._keep(self._row(speaker, line), capped=True)
|
|
73
90
|
|
|
@@ -77,9 +94,22 @@ class SessionTranscript:
|
|
|
77
94
|
label = f"[{speaker}]".ljust(_SPEAKER_WIDTH)
|
|
78
95
|
return f"{self._clock()} {label}{line}".rstrip()
|
|
79
96
|
|
|
80
|
-
def _show(self, row: str) -> None:
|
|
97
|
+
def _show(self, row: str, line: str) -> None:
|
|
81
98
|
# 화면은 상한과 무관하다. 사람이 보고 있는 것을 잘라 낼 이유가 없다.
|
|
82
99
|
if self._live:
|
|
100
|
+
if self._color:
|
|
101
|
+
color = next(
|
|
102
|
+
(color for prefix, color in _LIVE_COLORS if line.startswith(prefix)),
|
|
103
|
+
"",
|
|
104
|
+
)
|
|
105
|
+
if line.lstrip().startswith("… +"):
|
|
106
|
+
color = "\x1b[33m"
|
|
107
|
+
# 시각·화자는 낮은 대비로 두고 본문은 터미널 기본색을 유지한다.
|
|
108
|
+
prefix_size = len(row) - len(line.rstrip())
|
|
109
|
+
row = (
|
|
110
|
+
f"{_MUTED}{row[:prefix_size]}{_RESET}"
|
|
111
|
+
f"{color}{row[prefix_size:]}{_RESET}"
|
|
112
|
+
)
|
|
83
113
|
print(row, flush=True)
|
|
84
114
|
|
|
85
115
|
def _keep(self, row: str, *, capped: bool) -> None:
|
|
@@ -6353,7 +6353,12 @@ def _plan_verify_dispatched_results(
|
|
|
6353
6353
|
for row in payload.get("workerDispatches") or []:
|
|
6354
6354
|
if not isinstance(row, dict):
|
|
6355
6355
|
continue
|
|
6356
|
-
|
|
6356
|
+
# critic 동수 라운드는 `kind: "critic"` 으로 나간다 — 결과 파일명은
|
|
6357
|
+
# 같은 `-plan-verify-r<N>-` 꼴이다. reverify 계열만 세면 동수가 있던
|
|
6358
|
+
# run 마다 critic 표가 "디스패치 기록 없음" 으로 오탐된다(2026-09-09,
|
|
6359
|
+
# fontsninja-v3-site dev-10627 planning 002).
|
|
6360
|
+
kind = str(row.get("kind") or "")
|
|
6361
|
+
if not (kind.startswith("reverify-r") or kind == "critic"):
|
|
6357
6362
|
continue
|
|
6358
6363
|
name = Path(str(row.get("workerResultPath") or "")).name
|
|
6359
6364
|
if "-plan-verify-r" not in name:
|