okstra 0.186.3 → 0.186.4

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.
@@ -865,7 +865,7 @@ The manifest-provided `lead-events-*.jsonl` file is the canonical record for str
865
865
 
866
866
  `activityContractVersion: 1` is an interpretation version for new `implementation-planning` artifacts. A run without that field remains a historical run and does not require activity events or an `agentActivity[]` projection.
867
867
 
868
- The shared `okstra report-finalize` entrypoint reads canonical activity before translation source checking. Under contract v3 its in-process `project-activity` step assembles the report, filtering events by run identity and validating activity ID order before `agentActivity[]` is published. A historical v2 manifest retains the in-place projection path. For a non-English report, the lead runs `token-usage`, `project-activity`, and `check-source` before translator dispatch, then resumes at `render-views` after the translation sidecar exists. Conformance compares the resulting `agentActivity[]` IDs, order, and core fields with the canonical events for every lead host.
868
+ The shared `okstra report-finalize` entrypoint reads canonical activity before translation source checking. Under contract v3 its in-process `project-activity` step assembles the report, filtering events by run identity and validating activity ID order before `agentActivity[]` is published. A schema-invalid composed record is still published so `validate-run` can scan it; the step itself fails. A historical v2 manifest retains the in-place projection path. A failed Phase 7 step does not skip later checks: `validate-run` still runs, and only `teardown-stages` is skipped. For a non-English report, the lead runs `token-usage`, `project-activity`, and `check-source` before translator dispatch, then resumes at `render-views` after the translation sidecar exists. Conformance compares the resulting `agentActivity[]` IDs, order, and core fields with the canonical events for every lead host.
869
869
 
870
870
  Approval blockers use `open`, `answered`, `resolved`, and `obsolete`. `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block approval or the next phase; the DISAGREE votes stay on the plan item as evidence. A response sidecar with a proceeding disposition unblocks the same way. `request-revision` and `reject` still withhold the next phase.
871
871
 
package/docs/cli.md CHANGED
@@ -862,7 +862,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
862
862
  | `okstra approval-decision <open\|resolve\|carry> --ledger <approval-decisions.json> …` | Write the lead-owned clarification and approval ledger. `open` validates classification-specific dispositions and complete option fields, `resolve` requires real `A-NNN` check references, and `carry` keeps prior resolved decisions outside the active clarification list. |
863
863
  | `okstra design-snapshot --narrative <report-narrative.md> --output <design-preparation.json>` | Detect implementation-planning design surfaces and write the detector-owned snapshot consumed by final report assembly. |
864
864
  | `okstra plan-verify --narrative <report-narrative.md> --state <plan-body-verification.json>` | Recompute the plan-body gate from the convergence-owned state before `data.json` publication. `--report <historical-data.json>` remains the v2 reader. |
865
- | `okstra report-finalize --project-root <dir> --run-manifest <path> --report <final-report.md>` | Run Phase 7 in the manifest's contract order. Contract v3 collects usage into team state, assembles all single-owner inputs into `data.json` once, then checks, renders, spawns follow-ups, validates, and tears down eligible stage worktrees. Contract v2 retains its historical in-place projection sequence as a read-only compatibility path. Stops at the first non-zero exit except deferred token collection, reports each step, and prints the ordered `--only` recovery tail. This is the shared path for every lead adapter. |
865
+ | `okstra report-finalize --project-root <dir> --run-manifest <path> --report <final-report.md>` | Run Phase 7 in the manifest's contract order. Contract v3 collects usage into team state, assembles all single-owner inputs into `data.json` once, then checks, renders, spawns follow-ups, validates, and tears down eligible stage worktrees. Contract v2 retains its historical in-place projection sequence as a read-only compatibility path. A failed step still runs every later check through `validate-run`; only `teardown-stages` is skipped so a failed run does not reclaim worktrees. Reports each step and prints the ordered `--only` recovery tail from the earliest failure. This is the shared path for every lead adapter. |
866
866
  | `okstra render-views <final-report.data.json\|final-report.md>` | The Phase 7 `render-views` step, runnable on its own. Schema v2 data is rendered directly, and schema v3 data uses the same always-generated, task-specific human HTML path. The full reading copy uses `templates/reports/final-report-v2.template.md` and is rendered on demand with `okstra render-final-report`. Passing the Markdown sibling locates the same data.json. Schema v1 and quick reports keep the legacy conditional renderer. The Node wrapper calls `scripts/okstra-render-report-views.py`; `validators/validate-report-views.py` verifies source/schema/template digests, required human fields, form controls, external assets, diagram/table ID parity, and Response ID parity. |
867
867
  | `okstra design-prep <list\|show\|write>` | Review AI-prepared implementation design requests, inspect their effective confirmed response, or append a confirmed user/wizard response without editing the planning report |
868
868
  | `okstra wizard <init\|step\|render-args\|confirmation\|outcome> --state-file <path>` | Interactive input state machine for okstra-run, implemented by `okstra_ctl.wizard`. Seed a state file with `init`, then repeatedly call `step --answer <val>` to receive the next `Prompt` JSON. `--answer` is **required**; use `--no-submit` to peek at the next prompt without submitting a response. A `pick` with more choices than the host picker can display keeps `kind: "pick"` but adds `presentation: "numbered-text"`; render every option as a numbered Markdown list and submit the user's 1-based number, exact value, or exact label. Invalid, out-of-range, and ambiguous answers re-prompt without dropping choices. `render-args` returns the final `render-bundle` argument map, and `confirmation` returns the user echo block. On a completed wizard, `outcome` returns `renderArgs`, `persistActions`, and `confirmationText` together; project/global release-handoff PR-template persistence appears as `persistActions[].command == "config.set"`. For an `implementation` task type, `stage_pick` follows `approved_plan_pick` and selects the stage before `executor_pick`. The brief step appears only for entry task types—requirements-discovery, error-analysis, improvement-discovery, project-analysis, feature-analysis, and change-impact-analysis. Analysis inputs use `feature_evidence_pick` / `feature_evidence`, `project_evidence_pick` / `project_evidence`, and `analysis_target_pick` / `analysis_target`; a revision-requested report prioritizes its same-task, same-type rerun. Downstream lifecycle phases automatically carry the manifest brief, with a three-option `brief_carry` fallback when none is registered; `release-handoff` has no brief and enters multi-select `handoff_stage_pick` for eligible stage groups or the whole task |
@@ -343,7 +343,7 @@ Important modules:
343
343
  | `plan_derivations.py` | the supersession sweep `_common-contract.md` requires an author to do by hand — extracts the symbols, paths, and ids an answered clarification names and reports every plan string that mentions one. Advisory: it locates candidates and never judges which are now false |
344
344
  | `scope_provenance.py` | single source of truth for the scope-provenance grammar every phase-emitted requirement must declare, shared by `validators/validate-run.py` and `validators/validate_fanout.py` so the planning report and fan-out packets cannot drift |
345
345
  | `worker_artifact_paths.py` | canonical worker artifact path derivation (e.g. `audit_sidecar_rel` inserts `-audit-` after the first `-worker-` token), so dispatch and validation agree on non-canonical-path rejection |
346
- | `report_finalize.py` | Phase 7 post-report sequence **SSOT** — runs `check-source` → `token-usage` → `render-views` → `spawn-followups` → `validate-run` in that load-bearing order, stops at the first non-zero exit and names the failing step except `token-usage`, whose failure defers so the later steps still produce their artifacts. Both lead paths converge here: the Codex adapter calls it in-process (`codex_dispatch`), a Claude-led run reaches it through `okstra report-finalize`. Neither reimplements the sequence |
346
+ | `report_finalize.py` | Phase 7 post-report sequence **SSOT** — runs `check-source` → `token-usage` → `render-views` → `spawn-followups` → `validate-run` in that load-bearing order. A non-zero exit still runs every later check through `validate-run` and names the earliest failure; `teardown-stages` is skipped when any earlier step failed. Both lead paths converge here: the Codex adapter calls it in-process (`codex_dispatch`), a Claude-led run reaches it through `okstra report-finalize`. Neither reimplements the sequence |
347
347
  | `wrapper_status.py` | worker wrapper status sidecar reader — the host-side reader of the sidecar `worker_runner.py` writes. `is_terminal` is the one question it answers for the dispatch record and the pane reclaim: does `stage` read `exited` |
348
348
  | `worker_runner.py` | runs one worker CLI and records what happened — shared by every provider entrypoint. Owns the `selectors` pump over the child's streams, the stream-arrival idle watchdog (`killpg` on breach), the run-wide progress cap on the log copy, and the status sidecar's whole life. A run that dies after launch still closes its sidecar, so `worker_liveness` never reads a dead worker as running |
349
349
  | `session_transcript.py` | worker session transcript — one line per event (time, speaker, body) with a run-wide progress-line cap (`LOG_LINE_CAP`, elision notice) so a single-file dispatch's tool echo cannot dominate the project's `.okstra/` bytes; the fixed shape lets a later lead write share the same file |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.186.3",
3
+ "version": "0.186.4",
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.186.3",
3
- "builtAt": "2026-08-24T04:42:58.072Z",
2
+ "package": "0.186.4",
3
+ "builtAt": "2026-08-24T05:51:42.489Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -464,7 +464,12 @@ def assemble_report(
464
464
  project_root: Path, manifest_path: Path, *,
465
465
  replace: Callable[[str, str], None] = os.replace,
466
466
  ) -> dict[str, Any]:
467
- """모든 입력이 유효할 때만 계약 3.0 정본을 번 교체한다."""
467
+ """계약 3.0 정본을 조립해 게시한다.
468
+
469
+ 입력이 없어 조립 자체가 안 되면 기존 파일을 건드리지 않는다. 조립은
470
+ 됐는데 스키마만 깨진 기록은 게시한 뒤 거부한다. 게시를 건너뛰면
471
+ `validate-run` 이 이번 런 내용을 보지 못한다.
472
+ """
468
473
  manifest_row = ReportInputPath("run-manifest", "orchestrator", manifest_path)
469
474
  manifest = _read_json(manifest_row)
470
475
  if not uses_report_contract_v3(manifest):
@@ -490,9 +495,13 @@ def assemble_report(
490
495
  raise ReportAssemblyError(tuple(input_issues))
491
496
  data = _compose(project_root, manifest_path, manifest, inputs, schema)
492
497
  errors = validate(data, schema)
498
+ value = manifest.get("expectedReportRecordPath")
499
+ if not isinstance(value, str) or not value:
500
+ _fail("orchestrator", manifest_path, "expectedReportRecordPath", "required")
501
+ target = Path(value)
502
+ target = target if target.is_absolute() else project_root / target
503
+ _publish(target, data, replace)
493
504
  if errors:
494
- target = Path(str(manifest.get("expectedReportRecordPath") or "data.json"))
495
- target = target if target.is_absolute() else project_root / target
496
505
  raise ReportAssemblyError(
497
506
  tuple(
498
507
  AssemblyIssue(
@@ -504,10 +513,4 @@ def assemble_report(
504
513
  for error in errors
505
514
  )
506
515
  )
507
- value = manifest.get("expectedReportRecordPath")
508
- if not isinstance(value, str) or not value:
509
- _fail("orchestrator", manifest_path, "expectedReportRecordPath", "required")
510
- target = Path(value)
511
- target = target if target.is_absolute() else project_root / target
512
- _publish(target, data, replace)
513
516
  return data
@@ -7,17 +7,21 @@ read-only compatibility. The order is load-bearing: rendering before assembly
7
7
  or usage collection publishes stale derived views, and validating before
8
8
  rendering trips the report-views contract.
9
9
 
10
- `token-usage` is the one step whose failure does not stop the sequence, which
11
- deliberately accepts that first state: its input is the lead session log, so a
12
- refusal there would otherwise delete every artifact the later steps produce.
13
- The run does not pass in that state. `validators/validate-run.py` re-collects
14
- when the recorded usage is all zeros against an `unavailable` session source
15
- (`_needs_token_autofix`) and refuses the run rather than ship zeroed counts
16
- (`accuracy-failed`); a legacy v1 report is caught earlier still, by its
17
- unsubstituted `{{...}}` placeholders, which a v2 report never carries because
18
- its numeric cells are `null` until this step fills them. Substituting the
19
- tokens on a later retry then leaves the already-rendered html stale for
20
- `validators/validate-report-views.py`.
10
+ A non-zero exit no longer stops the sequence before `validate-run`. A schema
11
+ cap used to hide the contract scan: assembly refused, later steps never ran,
12
+ and the lead only saw those failures after a second finalize. Each step still
13
+ records its own exit; `ok` stays False when any step failed. `teardown-stages`
14
+ is the exception it reclaims worktrees, so a failed prefix skips it.
15
+
16
+ `token-usage` reads the lead session log (state outside this run). A refusal
17
+ there used to delete every later artifact; it no longer does, because the
18
+ sequence continues. `validators/validate-run.py` re-collects when the recorded
19
+ usage is all zeros against an `unavailable` session source (`_needs_token_autofix`)
20
+ and refuses the run rather than ship zeroed counts (`accuracy-failed`). A
21
+ legacy v1 report is caught earlier still, by its unsubstituted `{{...}}`
22
+ placeholders, which a v2 report never carries because its numeric cells are
23
+ `null` until this step fills them. Substituting the tokens on a later retry
24
+ then leaves the already-rendered html stale for `validators/validate-report-views.py`.
21
25
 
22
26
  The translation sidecar is NOT one of these steps. `render-views` overlays it,
23
27
  so a non-English run dispatches the translator before this sequence starts —
@@ -447,26 +451,14 @@ def run_finalize(
447
451
  before_step: Callable[[str], None] | None = None,
448
452
  only: Sequence[str] | None = None,
449
453
  ) -> dict[str, Any]:
450
- """Run the Phase 7 steps in contractual order.
451
-
452
- A non-zero exit stops the sequence, with one exception: ``token-usage``
453
- defers. Its input is the lead session log — state outside this run — so a
454
- refusal there is not evidence that the artifacts after it are unwritable.
455
- The deferred failure becomes the result's ``reason`` when nothing later
456
- fails, and yields to any later failure, which is the step that actually
457
- blocked the run.
458
-
459
- ``before_step`` fires immediately before each step is spawned, letting an
460
- adapter settle state the step will read (the Codex adapter marks the report
461
- writer `completed` before `validate-run` inspects team-state).
462
-
463
- ``only`` restricts the run to the named steps, preserving contractual
464
- order. The last step (`validate-run`) is the one that usually fails, and
465
- re-running the whole sequence to retry it repeats every preceding idempotent
466
- step at full token and wall-clock cost.
454
+ """계약 순서로 Phase 7 돌리고, 실패해도 `validate-run` 까지 모은다.
455
+
456
+ `before_step` 단계 프로세스 직전에 호출된다. Codex 어댑터가
457
+ `validate-run` 앞에 작성기 상태를 `completed` 표시하는 자리이다.
458
+
459
+ `only` 고른 단계만 계약 순서로 남긴다.
467
460
  """
468
461
  steps: list[dict[str, Any]] = []
469
- deferred = ""
470
462
  try:
471
463
  if ctx.report_contract_version != "3.0":
472
464
  write_execution_roles(ctx)
@@ -476,7 +468,9 @@ def run_finalize(
476
468
 
477
469
  if only:
478
470
  selected = set(only)
479
- contract_order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
471
+ contract_order = (
472
+ V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
473
+ )
480
474
  unknown = sorted(selected - set(contract_order))
481
475
  if unknown:
482
476
  return {
@@ -486,75 +480,70 @@ def run_finalize(
486
480
  }
487
481
  commands = [(name, cmd) for name, cmd in commands if name in selected]
488
482
 
483
+ first_failure = ""
489
484
  for name, command in commands:
485
+ # 실패한 시퀀스가 worktree 를 거두면 재작업 대상이 사라진다.
486
+ if name == STEP_TEARDOWN_STAGES and first_failure:
487
+ continue
490
488
  if before_step is not None:
491
489
  before_step(name)
492
- if name == STEP_PROJECT_ACTIVITY:
493
- try:
494
- if ctx.report_contract_version == "3.0":
495
- assembled = assemble_report(ctx.project_root, ctx.manifest_path)
496
- count = len(assembled.get("agentActivity") or [])
497
- else:
498
- rows = project_agent_activity(
499
- ctx.project_root,
500
- ctx.manifest_path,
501
- ctx.data_path,
502
- )
503
- count = len(rows)
504
- except (ActivityProjectionError, ReportAssemblyError) as exc:
505
- result = subprocess.CompletedProcess(
506
- command,
507
- 1,
508
- "",
509
- str(exc),
510
- )
511
- else:
512
- result = subprocess.CompletedProcess(
513
- command,
514
- 0,
515
- json.dumps({"count": count}),
516
- "",
517
- )
518
- else:
519
- result = None
520
- if name == STEP_TEARDOWN_STAGES:
521
- result = _teardown_stage_worktrees(ctx, command)
522
- if name == STEP_VALIDATE_RUN:
523
- try:
524
- _link_lead_result_for_validation(ctx)
525
- except (DispatchError, OSError, json.JSONDecodeError) as exc:
526
- return {
527
- "ok": False,
528
- "reason": f"lead result linkage failed: {exc}",
529
- "steps": steps,
530
- }
531
- if result is None:
532
- result = subprocess.run(
533
- command,
534
- cwd=ctx.project_root,
535
- text=True,
536
- capture_output=True,
537
- )
490
+ result = _run_finalize_step(ctx, name, command)
538
491
  steps.append(step_payload(name, command, result))
539
- if result.returncode != 0:
540
- failure = f"{name} failed with exit code {result.returncode}"
541
- # Token collection reads the lead session log, which lives outside
542
- # this run. One refusal there (`grandTotalTokens=0`) took the whole
543
- # run's output with it: html never rendered, follow-ups never
544
- # spawned, and `validate-run` then blocked on `report-views:
545
- # missing html artifact`. So this step alone defers instead of
546
- # stopping the sequence. It hides nothing — the non-zero exit stays
547
- # in `steps`, `ok` stays False, and the closing `validate-run`
548
- # still refuses the run.
549
- if name == STEP_TOKEN_USAGE:
550
- deferred = failure
551
- continue
552
- return {"ok": False, "reason": failure, "steps": steps}
553
- if deferred:
554
- return {"ok": False, "reason": deferred, "steps": steps}
492
+ if result.returncode != 0 and not first_failure:
493
+ first_failure = f"{name} failed with exit code {result.returncode}"
494
+ if first_failure:
495
+ return {"ok": False, "reason": first_failure, "steps": steps}
555
496
  return {"ok": True, "reason": "", "steps": steps}
556
497
 
557
498
 
499
+ def _run_finalize_step(
500
+ ctx: FinalizeContext,
501
+ name: str,
502
+ command: Sequence[str],
503
+ ) -> subprocess.CompletedProcess[str]:
504
+ """한 Phase 7 단계를 실행하고 그 단계의 종료 코드만 돌려준다."""
505
+ if name == STEP_PROJECT_ACTIVITY:
506
+ return _run_project_activity(ctx, command)
507
+ if name == STEP_TEARDOWN_STAGES:
508
+ return _teardown_stage_worktrees(ctx, command)
509
+ if name == STEP_VALIDATE_RUN:
510
+ try:
511
+ _link_lead_result_for_validation(ctx)
512
+ except (DispatchError, OSError, json.JSONDecodeError) as exc:
513
+ return subprocess.CompletedProcess(
514
+ command, 1, "", f"lead result linkage failed: {exc}"
515
+ )
516
+ return subprocess.run(
517
+ command,
518
+ cwd=ctx.project_root,
519
+ text=True,
520
+ capture_output=True,
521
+ )
522
+
523
+
524
+ def _run_project_activity(
525
+ ctx: FinalizeContext,
526
+ command: Sequence[str],
527
+ ) -> subprocess.CompletedProcess[str]:
528
+ """계약 3.0 은 조립, 그 외는 활동 투영만 한다."""
529
+ try:
530
+ if ctx.report_contract_version == "3.0":
531
+ assembled = assemble_report(ctx.project_root, ctx.manifest_path)
532
+ count = len(assembled.get("agentActivity") or [])
533
+ else:
534
+ rows = project_agent_activity(
535
+ ctx.project_root,
536
+ ctx.manifest_path,
537
+ ctx.data_path,
538
+ )
539
+ count = len(rows)
540
+ except (ActivityProjectionError, ReportAssemblyError) as exc:
541
+ return subprocess.CompletedProcess(command, 1, "", str(exc))
542
+ return subprocess.CompletedProcess(
543
+ command, 0, json.dumps({"count": count}), ""
544
+ )
545
+
546
+
558
547
  def write_execution_roles(ctx: FinalizeContext) -> None:
559
548
  """Write the exact manifest role set onto the final-report data.json."""
560
549
  if not ctx.data_path.is_file() or not ctx.manifest_path.is_file():
@@ -685,13 +674,11 @@ def _recovery_step_names(
685
674
  """The steps a retry has to re-run: every step from the earliest failure on.
686
675
 
687
676
  Naming only the failed steps would prescribe half a recovery. `token-usage`
688
- defers, so `render-views` already wrote an html view from unsubstituted
689
- data; substituting the tokens on a retry leaves that view stale
677
+ can fail while `render-views` still writes html from unsubstituted data;
678
+ substituting the tokens on a retry leaves that view stale
690
679
  (`validators/validate-report-views.py` checks `source-sha256` against the
691
- md body). And a sequence that stopped early never reached `validate-run`,
692
- which is the step that decides whether the run is shippable. Resuming from
693
- the earliest failure redoes both while still skipping the prefix that
694
- succeeded — the saving `--only` exists for.
680
+ md body). Resuming from the earliest failure redoes the tail while skipping
681
+ the prefix that succeeded the saving `--only` exists for.
695
682
  """
696
683
  failed = {
697
684
  string_value(step.get("name"))