okstra 0.212.2 → 0.213.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 CHANGED
@@ -832,6 +832,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
832
832
  | `okstra convergence apply-critic-gaps --work-state <path> --results <path>` | Apply one verified coverage-critic batch after the main queue reaches a terminal state |
833
833
  | `okstra convergence apply-acceptance-critic --work-state <path> --results <path>` | Record the final-verification acceptance critic's confirm-or-downgrade accounting into `config.critic`. Takes `{schemaVersion, taskKey, mode: "acceptance-devils-advocate", provider, modelExecutionValue, candidates[]}` with one `{candidateId, verdict}` per candidate, `verdict` being `confirmed` or `downgraded`; `candidatesProposed` / `confirmedBlockers` / `downgradedToResidual` are derived here, not declared in the batch. `apply-critic-gaps` implements coverage merge/drop semantics and rejects this mode, so this is the only writer of the acceptance summary. Requires a terminal main queue and refuses a second application |
834
834
  | `okstra convergence finalize --work-state <path> --output <path>` | Materialize the terminal schema v1.3 convergence state |
835
+ | `okstra convergence skip --run-manifest <path> --reason <text>` | Write the run's convergence final state at the manifest's `convergenceStatePath` when no analyser, designer, planner, verifier, or critic attempt returned a result (an implementation stage stopped before verification, or every verifier ended without one). The state is auto-disabled with `config.autoDisabled: "no-analyser-dispatched"` and the lead's text in `config.autoDisabledReason`. Exits 2 when any of those attempts returned a result or is still open, or when a convergence state already exists |
835
836
  | `okstra convergence validate --state <path> --kind <working\|final>` | Validate replayable working state or a terminal final state |
836
837
  | `okstra convergence example --kind <groups\|round-results\|critic-results\|coverage-batch\|acceptance-batch>` | Print one deterministic valid input example as JSON. Each kind feeds one command: `groups` → `seed --groups`, `round-results` → `apply-round --results`, `coverage-batch` → `apply-critic-gaps --results`, `acceptance-batch` → `apply-acceptance-critic --results`. `critic-results` feeds nothing — it is the critic worker's own result document, and feeding it to `apply-critic-gaps` is rejected by design; that reducer takes the coverage batch the lead assembles from those candidates plus each analyser's vote, which is what `--kind coverage-batch` prints |
837
838
  | `okstra group-context init --project-root <root> --task-group <group>` | Write the task-group context skeleton `.okstra/briefs/<task-group>/group-context.md` from `templates/reports/group-context.template.md` and print the sections to fill. Exits 2 when the file already has human sections; a file okstra created with only its memory region gets the human skeleton inserted above that region. The filled document is validated by `validators/validate-brief.py` (frontmatter `type: group-context`), copied by preparation into `instruction-set/task-group-context.md`, and carried in the analysis packet's `## Task-Group Context` section ahead of the brief extract; the trailing `## Task Memory` region between `<!-- okstra:task-memory:begin -->` / `end` markers is written by `report-finalize` (`record-group-memory`) and reaches sibling tasks as `## Task-Group Memory`. Preparation refuses a file that still carries a `<...>` placeholder line, and a task-group without the file prepares as before |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.212.2",
3
+ "version": "0.213.0",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.212.2",
3
- "builtAt": "2026-10-06T12:30:05.264Z",
2
+ "package": "0.213.0",
3
+ "builtAt": "2026-10-06T12:54:53.696Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -115,6 +115,7 @@ Every `okstra` command the lead documents cite, grouped by phase, each spelled w
115
115
  | `okstra convergence apply-critic-gaps --work-state <path> --results <path>` | Apply the gap-verification batch once, before `finalize` | `convergence` "Coverage critic pass" |
116
116
  | `okstra convergence apply-acceptance-critic --work-state <path> --results <path>` | Record the final-verification acceptance-critic accounting | `convergence` "Acceptance critic pass" |
117
117
  | `okstra convergence finalize --work-state <path> --output <path>` | Write the public final state after every round and critic batch is applied | `convergence` "Round 1-N" |
118
+ | `okstra convergence skip --run-manifest <path> --reason <text>` | Close a run where no analyser, verifier, or critic returned a result (e.g. the executor stopped and the user chose not to verify): writes the auto-disabled final state with your reason. Refused once any of those workers returned a result or is still running | `_implementation-deliverable.md` "Lead post-stage persistence" |
118
119
  | `okstra convergence validate --state <path> --kind <kind>` | Replay a persisted state's invariants | `convergence` "Convergence Test" |
119
120
 
120
121
  ### Phase 6 — synthesis and plan-body verification
@@ -18,6 +18,7 @@ from .convergence_engine import (
18
18
  apply_critic_gap_results,
19
19
  apply_round_results,
20
20
  finalize_working_state,
21
+ lead_only_final_state,
21
22
  plan_next_round,
22
23
  seed_working_state,
23
24
  validate_final_state,
@@ -338,6 +339,7 @@ _CLI_EPILOG = r"""Usage:
338
339
  okstra convergence apply-acceptance-critic --work-state <path> \
339
340
  --results <path>
340
341
  okstra convergence finalize --work-state <path> --output <path>
342
+ okstra convergence skip --run-manifest <path> --reason <text>
341
343
  okstra convergence validate --state <path> --kind <working|final>
342
344
  okstra convergence example --kind <groups|round-results|critic-results|
343
345
  coverage-batch|acceptance-batch>
@@ -558,6 +560,25 @@ def _parser() -> argparse.ArgumentParser:
558
560
  finalize.add_argument("--work-state", type=Path, required=True)
559
561
  finalize.add_argument("--output", type=Path, required=True)
560
562
 
563
+ skip = subparsers.add_parser(
564
+ "skip",
565
+ help="close a run no analysis or verification worker returned a result for",
566
+ description=(
567
+ "Write this run's convergence final state as auto-disabled "
568
+ "(`no-analyser-dispatched`) with the lead's reason, at the run "
569
+ "manifest's `convergenceStatePath`. Refused while any analyser, "
570
+ "designer, planner, verifier, or critic attempt returned a result "
571
+ "or is still open — those runs converge through `prepare-groups`. "
572
+ "Refused when a convergence state already exists."
573
+ ),
574
+ formatter_class=argparse.RawDescriptionHelpFormatter,
575
+ )
576
+ skip.add_argument("--run-manifest", type=Path, required=True)
577
+ skip.add_argument(
578
+ "--reason", required=True,
579
+ help="why no worker was dispatched or none returned a result",
580
+ )
581
+
561
582
  validate = subparsers.add_parser("validate", help="validate a persisted state")
562
583
  validate.add_argument("--state", type=Path, required=True)
563
584
  validate.add_argument("--kind", choices=("working", "final"), required=True)
@@ -1148,6 +1169,83 @@ def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
1148
1169
  return "finalized", args.output
1149
1170
 
1150
1171
 
1172
+ # 수렴 입력이 되는 결과를 내는 역할. 실행자·리드·리포트 작성자·번역자의
1173
+ # 결과는 수렴 대상이 아니다.
1174
+ _CONVERGENCE_SOURCE_ROLES = frozenset(
1175
+ {"analyser", "designer", "planner", "verifier", "critic"}
1176
+ )
1177
+
1178
+
1179
+ def _convergence_source_blockers(manifest: ExecutionManifest) -> list[str]:
1180
+ """결과를 냈거나 아직 열린 수렴 입력 역할의 attempt."""
1181
+ roles = {row.role_execution_ref: row.role for row in manifest.role_executions}
1182
+ latest: dict[str, Any] = {}
1183
+ for attempt in manifest.attempts:
1184
+ current = latest.get(attempt.invocation_ref)
1185
+ if current is None or attempt.attempt > current.attempt:
1186
+ latest[attempt.invocation_ref] = attempt
1187
+ blockers: list[str] = []
1188
+ for invocation in manifest.invocations:
1189
+ if roles.get(invocation.role_execution_ref) not in _CONVERGENCE_SOURCE_ROLES:
1190
+ continue
1191
+ attempt = latest.get(invocation.invocation_ref)
1192
+ if attempt is None:
1193
+ continue
1194
+ if attempt.status == "ok" and attempt.result_path:
1195
+ blockers.append(
1196
+ f"`{invocation.role_execution_ref}` returned a result "
1197
+ f"({invocation.invocation_ref})"
1198
+ )
1199
+ elif attempt.finished_at is None or attempt.status == "started":
1200
+ blockers.append(
1201
+ f"`{invocation.role_execution_ref}` is still running "
1202
+ f"({invocation.invocation_ref}); close it with `okstra team await`"
1203
+ )
1204
+ return blockers
1205
+
1206
+
1207
+ def _skip(args: argparse.Namespace) -> tuple[str, Path]:
1208
+ reason = str(args.reason).strip()
1209
+ if not reason:
1210
+ raise ConvergenceContractError(
1211
+ "--reason must say why no worker result exists"
1212
+ )
1213
+ authority = validated_run_authority(args.run_manifest)
1214
+ if authority.execution_manifest is None:
1215
+ raise ConvergenceContractError(
1216
+ "skip reads worker outcomes from a v2 execution manifest; a legacy "
1217
+ "v1 roster has no attempt ledger to read"
1218
+ )
1219
+ output = canonical_run_state_artifact(
1220
+ authority,
1221
+ manifest_field="convergenceStatePath",
1222
+ prefix="convergence",
1223
+ label="convergence state",
1224
+ )
1225
+ if output.exists():
1226
+ raise ConvergenceContractError(
1227
+ f"a convergence state already exists: {output}"
1228
+ )
1229
+ blockers = _convergence_source_blockers(authority.execution_manifest)
1230
+ if blockers:
1231
+ raise ConvergenceContractError(
1232
+ "skip is only for a run with no analysis or verification result; "
1233
+ + "; ".join(blockers)
1234
+ + " — converge through `okstra convergence prepare-groups` instead"
1235
+ )
1236
+ output.parent.mkdir(parents=True, exist_ok=True)
1237
+ write_final_state_atomic(
1238
+ output,
1239
+ lead_only_final_state(
1240
+ authority.task_key,
1241
+ auto_disabled="no-analyser-dispatched",
1242
+ reason=reason,
1243
+ ),
1244
+ migration=None,
1245
+ )
1246
+ return "skipped", output
1247
+
1248
+
1151
1249
  def _validate(args: argparse.Namespace) -> tuple[str, Path]:
1152
1250
  state = load_owned_json_object(args.state)
1153
1251
  errors = (
@@ -1783,6 +1881,7 @@ def _execute(args: argparse.Namespace) -> tuple[Any, ...]:
1783
1881
  "apply-critic-gaps": _apply_critic_gaps,
1784
1882
  "apply-acceptance-critic": _apply_acceptance_critic,
1785
1883
  "finalize": _finalize,
1884
+ "skip": _skip,
1786
1885
  "validate": _validate,
1787
1886
  }
1788
1887
  return operations[args.operation](args)
@@ -849,7 +849,12 @@ def validate_working_state(state: Mapping[str, Any]) -> list[str]:
849
849
  return errors
850
850
 
851
851
 
852
- def lead_only_final_state(task_key: str) -> dict[str, Any]:
852
+ def lead_only_final_state(
853
+ task_key: str,
854
+ *,
855
+ auto_disabled: str = "fewer-than-two-analysers",
856
+ reason: str | None = None,
857
+ ) -> dict[str, Any]:
853
858
  """워커를 띄우지 않는 phase 의 수렴 최종 상태.
854
859
 
855
860
  리포트 계약 3.0 은 모든 리포트에 수렴 입력을 요구한다(ADR-0020: 사실마다
@@ -861,18 +866,24 @@ def lead_only_final_state(task_key: str) -> dict[str, Any]:
861
866
 
862
867
  "수렴이 돌지 않았다" 는 부재가 아니라 사실이므로 파일로 남긴다 — 분석자가
863
868
  둘 미만이어서 자동 비활성이라는 뜻이고, 그게 이 어휘의 `auto-disabled` 다.
869
+
870
+ 로스터에 검증자가 있는데 하나도 결과를 내지 않은 run 은 `okstra convergence
871
+ skip` 이 `no-analyser-dispatched` 와 리드가 적은 사유로 같은 상태를 쓴다.
864
872
  """
873
+ config: dict[str, Any] = {
874
+ "enabled": False,
875
+ "adversarial": False,
876
+ "maxRounds": 1,
877
+ "effectiveMaxRounds": 1,
878
+ "verificationMode": "lightweight",
879
+ "autoDisabled": auto_disabled,
880
+ }
881
+ if reason is not None:
882
+ config["autoDisabledReason"] = reason
865
883
  state = {
866
884
  "schemaVersion": FINAL_SCHEMA_VERSION,
867
885
  "taskKey": task_key,
868
- "config": {
869
- "enabled": False,
870
- "adversarial": False,
871
- "maxRounds": 1,
872
- "effectiveMaxRounds": 1,
873
- "verificationMode": "lightweight",
874
- "autoDisabled": "fewer-than-two-analysers",
875
- },
886
+ "config": config,
876
887
  "findings": [],
877
888
  "roundHistory": [],
878
889
  "round2SkippedReason": "auto-disabled",
@@ -63,6 +63,7 @@ are collected and convergence finished. Phase 1-5 do not need it.
63
63
  - Parse the executor's `### Stage Carry Evidence` JSON block. If absent or unparsable, end with status `contract-violated` and route to a follow-up `error-analysis`.
64
64
  - The `### Stage Carry Evidence` JSON may include `designPrepEvidence[]`. Emit a row only when this stage produced concrete evidence that refines an effective PREP item: `itemId`, the injected `assessmentFingerprint`, `resolution`, and non-empty `evidence[]` are required; `overrides` is optional and only records observed, non-authoritative refinements. Carry evidence never represents user approval. Downstream resolution accepts it only from transitive dependency stages with the matching fingerprint.
65
65
  - **A `FAIL` synthesised verdict withholds the two writes below.** They are what marks the stage `done`, so performing them on a stage whose verifier found a blocking defect stacks the next stage on a confirmed regression. When the synthesised verdict is `FAIL`: write NO carry sidecar, and append a `status:"failed"` row in place of the `done` row — same `okstra_ctl.consumers.append_consumer` call, carrying `report_path` and the SHA of HEAD. That row is terminal *without* completion: dependent stages stay blocked because this stage is not done, while its worktree-registry occupancy is released so a fix run can re-enter the same stage number — `--stage <N>` reuses the preserved worktree and branch instead of provisioning a new one. State the reason in the report's `Stage sidecar evidence` section as `withheld`. **Enforced:** `validators/validate-run.py` `_validate_stage_carry_sidecar_exists` accepts a missing carry file only when that field is non-empty, so silently skipping the sidecar still fails the run.
66
+ - **A run that ends with no verifier result still closes.** When the stage ends before any verifier returned a verdict — the executor stopped without a change and the user chose not to verify, or every dispatched verifier ended without a result — append the `failed` row as above, record each roster verifier `not-run` with its reason (`okstra worker-state transition --team-state <path> --worker <worker-id> --status not-run --reason <text>`; a worker the dispatch skipped already carries one), and write the convergence state with `okstra convergence skip --run-manifest <path> --reason <text>` before dispatching the report writer. Without that state the report-writer packet refuses with `convergence … required source is missing`. The command refuses while any analyser, verifier, or critic attempt returned a result or is still running; such a run converges normally. In the report, write each verifier's `verifierResults[]` row as `verdict: not-run`, and state the reason under `Stage sidecar evidence` as `withheld`.
66
67
  - On a non-`FAIL` verdict, for this run's single stage: write its JSON verbatim to `runs/<impl-task-key>/carry/stage-<N>.json`. Refuse to overwrite an existing file (one stage = one sidecar; a fix run re-entering after a `failed` row writes the first one, because a withheld stage never wrote it). A stage reopened after a `done` row (see "Reopening a settled stage" below) still has the sidecar of the run that first settled it, naming a head the stage has since moved past: move it unchanged to `runs/<impl-task-key>/carry/superseded/stage-<N>-from-<that run's task-type>-<seq>.json` before writing the new one, and name both paths in `Stage sidecar evidence`. Do not rename it inside `carry/`: the readers glob `carry/stage-*.json` (`okstra_ctl.stage_map`, `okstra_ctl.consumers.backfill_done_from_carry`), so a renamed file there is read again as a live sidecar. A reopened stage whose run ends `FAIL` writes nothing and leaves the old file where it is.
67
68
  - On a non-`FAIL` verdict, for this run's single stage: append a `status:"done"` row to `runs/<plan-task-key>/consumers.jsonl` with `completed_at`, `carry_path`, `report_path` (this run's final-report path relative to the run root), and the SHA of HEAD. Append it with `okstra_ctl.consumers.append_consumer` (NOT a raw filesystem write) — that call honours the consumers lock AND releases this stage's worktree-registry occupancy, so later runs stop seeing a finished stage as a concurrent run. `report_path` lets `final-verification` cite each stage's originating report when assembling its Source Implementation Report list.
68
69
  - **Reopening a settled stage.** Appending a `status:"failed"` row after a `done` one withdraws that stage: `--stage N` accepts it again, its dependents stop resolving a base from the withdrawn head, and whole-task final-verification blocks until it is settled again. Use the same `okstra_ctl.consumers.append_consumer` call with a `reason`, and re-run the stage rather than repairing the tree outside okstra — a stage reverted outside the ledger leaves the recorded `head_commit` and the carry sidecar naming a tree that no longer exists, and the next stage branches from it.
@@ -17,8 +17,13 @@ def _validate_verifier_reran_independently(data: dict, failures: list[str]) -> N
17
17
 
18
18
  executor 인용 표현을 잡던 정규식 갈래는 삭제했다. 표현을 세는 검사라
19
19
  같은 재현을 어떻게 서술했느냐로 통과가 갈렸다.
20
+
21
+ `not-run` 행은 판정이 없는 검증자다. 재현할 주체가 없으니 칸을 요구하면
22
+ 작성자가 지어낸 문장으로 채우게 된다.
20
23
  """
21
24
  for who, row in _verifier_rows(data):
25
+ if row.get("verdict") == "not-run":
26
+ continue
22
27
  rerun = row.get("independentValidationRerun")
23
28
  if not isinstance(rerun, str) or not rerun.strip():
24
29
  failures.append(