okstra 0.128.0 → 0.129.1
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/architecture/storage-model.md +15 -2
- package/docs/architecture.md +21 -5
- package/docs/cli.md +13 -2
- package/docs/project-structure-overview.md +22 -7
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +3 -3
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +6 -6
- package/runtime/prompts/lead/adapters/claude-code.md +2 -1
- package/runtime/prompts/lead/adapters/codex.md +1 -0
- package/runtime/prompts/lead/adapters/external.md +1 -0
- package/runtime/prompts/lead/convergence.md +42 -84
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -8
- package/runtime/prompts/lead/report-writer.md +22 -28
- package/runtime/prompts/lead/team-contract.md +28 -11
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -0
- package/runtime/prompts/profiles/improvement-discovery.md +11 -1
- package/runtime/prompts/profiles/requirements-discovery.md +8 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -13
- package/runtime/python/okstra_ctl/codex_dispatch.py +70 -319
- package/runtime/python/okstra_ctl/context_cost.py +82 -7
- package/runtime/python/okstra_ctl/convergence.py +223 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +1152 -0
- package/runtime/python/okstra_ctl/convergence_migration.py +184 -0
- package/runtime/python/okstra_ctl/convergence_store.py +85 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -14
- package/runtime/python/okstra_ctl/improvement_assignment.py +61 -0
- package/runtime/python/okstra_ctl/path_hints.py +23 -1
- package/runtime/python/okstra_ctl/paths.py +10 -1
- package/runtime/python/okstra_ctl/render.py +56 -11
- package/runtime/python/okstra_ctl/report_finalize.py +394 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +6 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +107 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +102 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +79 -9
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +158 -0
- package/runtime/templates/implementation-worker-preamble.md +51 -0
- package/runtime/templates/operating-standard.md +4 -4
- package/runtime/templates/report-writer-prompt-preamble.md +37 -0
- package/runtime/templates/worker-error-contract.md +46 -0
- package/runtime/templates/worker-prompt-preamble.md +28 -234
- package/runtime/validators/lib/fixtures.sh +27 -12
- package/runtime/validators/validate-run.py +201 -72
- package/src/cli-registry.mjs +18 -0
- package/src/commands/execute/convergence.mjs +34 -0
- package/src/commands/report/finalize.mjs +65 -0
|
@@ -46,6 +46,9 @@ from okstra_ctl.md_table import ( # noqa: E402
|
|
|
46
46
|
split_pipe_row as _split_pipe_row,
|
|
47
47
|
)
|
|
48
48
|
from okstra_ctl.final_report_paths import final_report_data_path as _data_path_for # noqa: E402
|
|
49
|
+
from okstra_ctl.improvement_assignment import ( # noqa: E402
|
|
50
|
+
validate_primary_lens_assignments,
|
|
51
|
+
)
|
|
49
52
|
from okstra_ctl.design_prep import ( # noqa: E402
|
|
50
53
|
DesignPrepError,
|
|
51
54
|
_planning_seq as _design_prep_planning_seq,
|
|
@@ -59,8 +62,10 @@ from okstra_ctl.design_surfaces import ( # noqa: E402
|
|
|
59
62
|
expected_prep_plan_item_id,
|
|
60
63
|
)
|
|
61
64
|
from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
62
|
-
|
|
65
|
+
PromptRecord,
|
|
66
|
+
validate_initial_prompt_records,
|
|
63
67
|
)
|
|
68
|
+
from okstra_ctl.convergence_engine import validate_final_state # noqa: E402
|
|
64
69
|
|
|
65
70
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
66
71
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
@@ -370,45 +375,198 @@ def effective_run_task_type(run_manifest: dict, task_manifest: dict) -> str:
|
|
|
370
375
|
).strip()
|
|
371
376
|
|
|
372
377
|
|
|
373
|
-
def
|
|
378
|
+
def _resolve_prompt_record_path(project_root: Path, prompt_value: str) -> Path:
|
|
379
|
+
prompt_path = Path(prompt_value)
|
|
380
|
+
return prompt_path if prompt_path.is_absolute() else project_root / prompt_path
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _fallback_dispatch_kind(prompt_value: str) -> str:
|
|
384
|
+
match = re.search(r"-reverify-(r\d+)", Path(prompt_value).name)
|
|
385
|
+
return f"reverify-{match.group(1)}" if match else "initial"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _persisted_prompt_records(
|
|
389
|
+
*,
|
|
390
|
+
project_root: Path,
|
|
391
|
+
selected_ids: set[str],
|
|
392
|
+
team_state: dict,
|
|
393
|
+
) -> list[PromptRecord]:
|
|
394
|
+
dispatches = team_state.get("workerDispatches")
|
|
395
|
+
source = dispatches if isinstance(dispatches, list) and dispatches else (
|
|
396
|
+
team_state.get("workers") or []
|
|
397
|
+
)
|
|
398
|
+
records: list[PromptRecord] = []
|
|
399
|
+
seen: set[tuple[str, str, str]] = set()
|
|
400
|
+
for worker in source:
|
|
401
|
+
if not isinstance(worker, dict):
|
|
402
|
+
continue
|
|
403
|
+
worker_id = str(worker.get("workerId") or "").strip()
|
|
404
|
+
prompt_value = str(worker.get("promptPath") or "").strip()
|
|
405
|
+
if worker_id not in selected_ids or not prompt_value:
|
|
406
|
+
continue
|
|
407
|
+
dispatch_kind = str(worker.get("kind") or "").strip()
|
|
408
|
+
if not dispatch_kind:
|
|
409
|
+
dispatch_kind = _fallback_dispatch_kind(prompt_value)
|
|
410
|
+
identity = (worker_id, dispatch_kind, prompt_value)
|
|
411
|
+
if identity in seen:
|
|
412
|
+
continue
|
|
413
|
+
seen.add(identity)
|
|
414
|
+
records.append(
|
|
415
|
+
PromptRecord(
|
|
416
|
+
worker_id=worker_id,
|
|
417
|
+
dispatch_kind=dispatch_kind,
|
|
418
|
+
path=_resolve_prompt_record_path(project_root, prompt_value),
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
return records
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _markdown_section(text: str, heading: str) -> str:
|
|
425
|
+
pattern = rf"(?ms)^##\s+{re.escape(heading)}\s*$\n(.*?)(?=^##\s|\Z)"
|
|
426
|
+
match = re.search(pattern, text)
|
|
427
|
+
return match.group(1) if match else ""
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _parse_resolved_lenses(grilling_text: str) -> list[str]:
|
|
431
|
+
section = _markdown_section(grilling_text, "Resolved lenses")
|
|
432
|
+
return [
|
|
433
|
+
match.group(1).strip().strip("`")
|
|
434
|
+
for line in section.splitlines()
|
|
435
|
+
if (match := re.match(r"^\s*-\s+(.+?)\s*$", line))
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _parse_primary_assignments(
|
|
440
|
+
grilling_text: str,
|
|
441
|
+
) -> tuple[dict[str, str], list[str]]:
|
|
442
|
+
section = _markdown_section(grilling_text, "Primary Pass Assignments")
|
|
443
|
+
rows = [
|
|
444
|
+
(line.strip(), _split_pipe_row(line.strip()))
|
|
445
|
+
for line in section.splitlines()
|
|
446
|
+
if line.strip().startswith("|") and line.strip().endswith("|")
|
|
447
|
+
]
|
|
448
|
+
errors: list[str] = []
|
|
449
|
+
if not rows or rows[0][1] != ["Worker ID", "Primary lens"]:
|
|
450
|
+
return {}, [
|
|
451
|
+
"Primary Pass Assignments table must start with "
|
|
452
|
+
"`| Worker ID | Primary lens |`"
|
|
453
|
+
]
|
|
454
|
+
assignments: dict[str, str] = {}
|
|
455
|
+
for raw_line, row in rows[1:]:
|
|
456
|
+
if _is_markdown_separator(raw_line):
|
|
457
|
+
continue
|
|
458
|
+
if len(row) != 2 or not all(cell.strip() for cell in row):
|
|
459
|
+
errors.append("Primary Pass Assignments rows must contain two values")
|
|
460
|
+
continue
|
|
461
|
+
worker_id, lens = (cell.strip().strip("`") for cell in row)
|
|
462
|
+
if worker_id in assignments:
|
|
463
|
+
errors.append(f"duplicate worker assignment row: {worker_id}")
|
|
464
|
+
continue
|
|
465
|
+
assignments[worker_id] = lens
|
|
466
|
+
return assignments, errors
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _grilling_log_path_from_prompts(
|
|
470
|
+
*,
|
|
471
|
+
project_root: Path,
|
|
472
|
+
worker_ids: list[str],
|
|
473
|
+
records: list[PromptRecord],
|
|
474
|
+
) -> tuple[Path | None, list[str]]:
|
|
475
|
+
header = "**Phase 1.5 Grilling Log:**"
|
|
476
|
+
values: set[str] = set()
|
|
477
|
+
errors: list[str] = []
|
|
478
|
+
for record in records:
|
|
479
|
+
if record.worker_id not in worker_ids or record.dispatch_kind != "initial":
|
|
480
|
+
continue
|
|
481
|
+
try:
|
|
482
|
+
text = record.path.read_text(encoding="utf-8")
|
|
483
|
+
except OSError:
|
|
484
|
+
continue
|
|
485
|
+
matches = [
|
|
486
|
+
line.strip()[len(header):].strip().strip("`")
|
|
487
|
+
for line in text.splitlines()
|
|
488
|
+
if line.strip().startswith(header)
|
|
489
|
+
]
|
|
490
|
+
if len(matches) == 1 and matches[0]:
|
|
491
|
+
values.add(matches[0])
|
|
492
|
+
if not values:
|
|
493
|
+
return None, ["initial analyser prompts have no grilling-log path"]
|
|
494
|
+
if len(values) != 1:
|
|
495
|
+
return None, ["initial analyser prompts reference different grilling logs"]
|
|
496
|
+
return _resolve_prompt_record_path(project_root, values.pop()), errors
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _validate_improvement_primary_assignments(
|
|
500
|
+
*,
|
|
501
|
+
project_root: Path,
|
|
502
|
+
worker_ids: list[str],
|
|
503
|
+
records: list[PromptRecord],
|
|
504
|
+
) -> list[str]:
|
|
505
|
+
grilling_path, errors = _grilling_log_path_from_prompts(
|
|
506
|
+
project_root=project_root,
|
|
507
|
+
worker_ids=worker_ids,
|
|
508
|
+
records=records,
|
|
509
|
+
)
|
|
510
|
+
if grilling_path is None:
|
|
511
|
+
return errors
|
|
512
|
+
try:
|
|
513
|
+
grilling_text = grilling_path.read_text(encoding="utf-8")
|
|
514
|
+
except OSError as exc:
|
|
515
|
+
return [*errors, f"cannot read grilling log {grilling_path}: {exc}"]
|
|
516
|
+
lenses = _parse_resolved_lenses(grilling_text)
|
|
517
|
+
assignments, parse_errors = _parse_primary_assignments(grilling_text)
|
|
518
|
+
return [
|
|
519
|
+
*errors,
|
|
520
|
+
*parse_errors,
|
|
521
|
+
*validate_primary_lens_assignments(assignments, worker_ids, lenses),
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _validate_initial_analysis_prompts(
|
|
374
526
|
data: dict,
|
|
375
527
|
failures: list[str],
|
|
376
528
|
) -> None:
|
|
377
|
-
"""Validate
|
|
378
|
-
|
|
529
|
+
"""Validate persisted prompts using their functional audience contract."""
|
|
530
|
+
task_type = str(data.get("taskType") or "").strip()
|
|
531
|
+
if not task_type:
|
|
379
532
|
return
|
|
380
|
-
|
|
381
533
|
run_manifest = data.get("runManifest") or {}
|
|
382
534
|
team_contract = run_manifest.get("teamContract") or {}
|
|
383
535
|
selected_workers = team_contract.get("requiredWorkerRoles") or []
|
|
384
|
-
|
|
536
|
+
selected_worker_ids = [
|
|
385
537
|
str(worker.get("workerId") or "").strip()
|
|
386
538
|
for worker in selected_workers
|
|
387
539
|
if isinstance(worker, dict)
|
|
388
|
-
|
|
540
|
+
and str(worker.get("workerId") or "").strip()
|
|
541
|
+
]
|
|
542
|
+
selected_ids = set(selected_worker_ids)
|
|
389
543
|
project_root = Path(data["projectRoot"])
|
|
390
544
|
team_state = data.get("teamState") or {}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
545
|
+
manifest = dict(run_manifest)
|
|
546
|
+
manifest["taskType"] = task_type
|
|
547
|
+
records = _persisted_prompt_records(
|
|
548
|
+
project_root=project_root,
|
|
549
|
+
selected_ids=selected_ids,
|
|
550
|
+
team_state=team_state,
|
|
551
|
+
)
|
|
552
|
+
errors = validate_initial_prompt_records(
|
|
553
|
+
manifest=manifest,
|
|
554
|
+
records=records,
|
|
555
|
+
)
|
|
556
|
+
if task_type == "improvement-discovery":
|
|
557
|
+
errors.extend(
|
|
558
|
+
_validate_improvement_primary_assignments(
|
|
559
|
+
project_root=project_root,
|
|
560
|
+
worker_ids=[
|
|
561
|
+
worker_id
|
|
562
|
+
for worker_id in selected_worker_ids
|
|
563
|
+
if worker_id != "report-writer"
|
|
564
|
+
],
|
|
565
|
+
records=records,
|
|
566
|
+
)
|
|
407
567
|
)
|
|
408
|
-
|
|
409
|
-
errors = validate_final_verification_prompt_paths(prompt_paths)
|
|
410
568
|
failures.extend(
|
|
411
|
-
f"
|
|
569
|
+
f"{task_type} prompt contract: {error}" for error in errors
|
|
412
570
|
)
|
|
413
571
|
|
|
414
572
|
|
|
@@ -3135,63 +3293,34 @@ def _validate_forbidden_actions(
|
|
|
3135
3293
|
failures.extend(f"forbidden-action: {v}" for v in violations)
|
|
3136
3294
|
|
|
3137
3295
|
|
|
3138
|
-
|
|
3139
|
-
"
|
|
3140
|
-
"
|
|
3141
|
-
"
|
|
3142
|
-
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
def classification_counts_from_findings(findings) -> dict:
|
|
3147
|
-
"""Recompute `finalClassificationCounts` from `findings[].classification`.
|
|
3148
|
-
|
|
3149
|
-
Shared with `tests/contract/test_convergence_state_contract.py` so the
|
|
3150
|
-
invariant has one definition — it used to live only in that test, which
|
|
3151
|
-
meant it was checked against four hand-written fixtures and never against
|
|
3152
|
-
the state a lead actually writes.
|
|
3153
|
-
"""
|
|
3154
|
-
counts = {value: 0 for value in CLASSIFICATION_COUNT_KEYS.values()}
|
|
3155
|
-
for finding in findings or []:
|
|
3156
|
-
if not isinstance(finding, dict):
|
|
3157
|
-
continue
|
|
3158
|
-
key = CLASSIFICATION_COUNT_KEYS.get(finding.get("classification"))
|
|
3159
|
-
if key:
|
|
3160
|
-
counts[key] += 1
|
|
3161
|
-
return counts
|
|
3162
|
-
|
|
3296
|
+
_CONVERGENCE_INTERMEDIATE_PREFIXES = (
|
|
3297
|
+
"convergence-groups-",
|
|
3298
|
+
"convergence-work-",
|
|
3299
|
+
"convergence-round-",
|
|
3300
|
+
)
|
|
3163
3301
|
|
|
3164
|
-
def _validate_convergence_state_counts(run_dir, failures) -> None:
|
|
3165
|
-
"""A run's convergence state must agree with its own findings list.
|
|
3166
3302
|
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
artifact, so a miscount survived into the report as two different truths
|
|
3170
|
-
(observed on dev-6901 planning 009: declared partial 6 / worker-unique 4
|
|
3171
|
-
against an actual 5 / 5). Downstream readers pick one arbitrarily.
|
|
3172
|
-
"""
|
|
3303
|
+
def _validate_convergence_states(run_dir, failures) -> None:
|
|
3304
|
+
"""Replay shared engine invariants for every public final state artifact."""
|
|
3173
3305
|
from pathlib import Path as _Path
|
|
3174
3306
|
|
|
3175
3307
|
state_dir = _Path(run_dir) / "state"
|
|
3176
3308
|
if not state_dir.is_dir():
|
|
3177
3309
|
return
|
|
3178
3310
|
for state_path in sorted(state_dir.glob("convergence-*.json")):
|
|
3311
|
+
if state_path.name.startswith(_CONVERGENCE_INTERMEDIATE_PREFIXES):
|
|
3312
|
+
continue
|
|
3179
3313
|
try:
|
|
3180
3314
|
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
3181
3315
|
except (OSError, json.JSONDecodeError) as exc:
|
|
3182
|
-
failures.append(f"convergence state {state_path.name} unreadable: {exc}")
|
|
3183
|
-
continue
|
|
3184
|
-
declared = state.get("finalClassificationCounts")
|
|
3185
|
-
if not isinstance(declared, dict):
|
|
3186
|
-
continue
|
|
3187
|
-
actual = classification_counts_from_findings(state.get("findings"))
|
|
3188
|
-
if declared != actual:
|
|
3189
3316
|
failures.append(
|
|
3190
|
-
f"convergence state {state_path.name}:
|
|
3191
|
-
f"{declared} does not match findings[].classification {actual}. "
|
|
3192
|
-
"The counts are a summary of the findings list — a mismatch means "
|
|
3193
|
-
"the run reports two different classifications for the same work."
|
|
3317
|
+
f"convergence state {state_path.name}: unreadable JSON: {exc}"
|
|
3194
3318
|
)
|
|
3319
|
+
continue
|
|
3320
|
+
failures.extend(
|
|
3321
|
+
f"convergence state {state_path.name}: {error}"
|
|
3322
|
+
for error in validate_final_state(state)
|
|
3323
|
+
)
|
|
3195
3324
|
|
|
3196
3325
|
|
|
3197
3326
|
def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
|
|
@@ -3569,7 +3698,7 @@ def main() -> int:
|
|
|
3569
3698
|
# contain every required section. Substring checks below are a
|
|
3570
3699
|
# safety net for hand-edited or pre-v1.0 reports.
|
|
3571
3700
|
task_type = effective_run_task_type(run_manifest, task_manifest)
|
|
3572
|
-
|
|
3701
|
+
_validate_initial_analysis_prompts(
|
|
3573
3702
|
{
|
|
3574
3703
|
"taskType": task_type,
|
|
3575
3704
|
"projectRoot": project_root,
|
|
@@ -3641,7 +3770,7 @@ def main() -> int:
|
|
|
3641
3770
|
run_dir = report_path.parent.parent
|
|
3642
3771
|
_validate_requirements_discovery_fanout(run_dir, failures)
|
|
3643
3772
|
# Phase-agnostic: convergence runs in every finding-producing phase.
|
|
3644
|
-
|
|
3773
|
+
_validate_convergence_states(report_path.parent.parent, failures)
|
|
3645
3774
|
validate_report_views(report_path, failures)
|
|
3646
3775
|
|
|
3647
3776
|
validation_status = "passed" if not failures else "failed"
|
package/src/cli-registry.mjs
CHANGED
|
@@ -66,6 +66,13 @@ export const COMMAND_REGISTRY = [
|
|
|
66
66
|
category: "admin",
|
|
67
67
|
summary: ["Move legacy .project-docs/okstra/ to .okstra/ (one-shot)"],
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "convergence",
|
|
71
|
+
module: "./commands/execute/convergence.mjs",
|
|
72
|
+
export: "run",
|
|
73
|
+
category: "admin",
|
|
74
|
+
summary: ["Advance and validate deterministic re-verification state"],
|
|
75
|
+
},
|
|
69
76
|
{
|
|
70
77
|
name: "git-reconcile",
|
|
71
78
|
module: "./commands/execute/git-reconcile.mjs",
|
|
@@ -312,6 +319,17 @@ export const COMMAND_REGISTRY = [
|
|
|
312
319
|
category: "introspection",
|
|
313
320
|
summary: ["Render slim AI + self-contained HTML views of a final report"],
|
|
314
321
|
},
|
|
322
|
+
{
|
|
323
|
+
name: "report-finalize",
|
|
324
|
+
module: "./commands/report/finalize.mjs",
|
|
325
|
+
export: "run",
|
|
326
|
+
category: "introspection",
|
|
327
|
+
summary: [
|
|
328
|
+
"Run the Phase 7 post-report sequence (token-usage,",
|
|
329
|
+
"render-views, spawn-followups, validate-run) in its",
|
|
330
|
+
"contractual order against one final report.",
|
|
331
|
+
],
|
|
332
|
+
},
|
|
315
333
|
{
|
|
316
334
|
name: "render-final-report",
|
|
317
335
|
module: "./commands/report/render-final-report.mjs",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra convergence — advance deterministic re-verification state
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra convergence seed --groups <path> --work-state <path> \\
|
|
7
|
+
--final-state <path> --migration-dir <path> [--restart-from-round0]
|
|
8
|
+
okstra convergence plan-round --work-state <path> --plan <path>
|
|
9
|
+
okstra convergence apply-round --work-state <path> --plan <path> \\
|
|
10
|
+
--results <path>
|
|
11
|
+
okstra convergence finalize --work-state <path> --output <path>
|
|
12
|
+
okstra convergence validate --state <path> --kind <working|final>
|
|
13
|
+
|
|
14
|
+
This internal admin command is consumed by the Phase 5.5 lead contract. It
|
|
15
|
+
does not install or revive the obsolete okstra-convergence skill.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export async function run(args) {
|
|
19
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
20
|
+
process.stdout.write(USAGE);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (args.length === 0) {
|
|
24
|
+
process.stdout.write(USAGE);
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
const result = await runPythonModule({
|
|
28
|
+
module: "okstra_ctl.convergence",
|
|
29
|
+
args,
|
|
30
|
+
stdio: "inherit-stdout",
|
|
31
|
+
});
|
|
32
|
+
return result.code;
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
import { resolvePaths } from "../../lib/paths.mjs";
|
|
3
|
+
|
|
4
|
+
const USAGE = `okstra report-finalize — run the Phase 7 post-report sequence
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
okstra report-finalize --project-root <dir> --run-manifest <path> \\
|
|
8
|
+
--report <final-report-<task-type>-<seq>.md> [--team-state <path>]
|
|
9
|
+
|
|
10
|
+
Runs the four Phase 7 steps in their contractual order against one final-report:
|
|
11
|
+
|
|
12
|
+
1. token-usage substitute real token/cost numbers into the data.json
|
|
13
|
+
2. render-views write the self-contained *.html sibling (skipped when the
|
|
14
|
+
report has no C-* clarification rows and is not a plan
|
|
15
|
+
approval target)
|
|
16
|
+
3. spawn-followups turn section 4 rows into task stubs
|
|
17
|
+
4. validate-run validate the finished run artifacts
|
|
18
|
+
|
|
19
|
+
Every step is idempotent, so re-running after a fixed failure is safe. The
|
|
20
|
+
sequence stops at the first non-zero exit and reports which step failed.
|
|
21
|
+
|
|
22
|
+
This is the same code path the Codex lead adapter runs automatically after its
|
|
23
|
+
report-writer completes, so a Claude-led and a Codex-led run finalize
|
|
24
|
+
identically.
|
|
25
|
+
|
|
26
|
+
--workspace-root is owned by this command.
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const OWNED_FLAGS = new Set(["--workspace-root"]);
|
|
30
|
+
|
|
31
|
+
function isOwnedFlag(arg) {
|
|
32
|
+
if (OWNED_FLAGS.has(arg)) return true;
|
|
33
|
+
return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildReportFinalizeArgs(args, paths) {
|
|
37
|
+
return ["--workspace-root", paths.workspace, ...args];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function run(args) {
|
|
41
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
42
|
+
process.stdout.write(USAGE);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
if (args.length === 0) {
|
|
46
|
+
process.stdout.write(USAGE);
|
|
47
|
+
return 2;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const forbidden = args.find(isOwnedFlag);
|
|
51
|
+
if (forbidden) {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`error: ${forbidden} is set by 'okstra report-finalize' itself — remove it from your args\n`,
|
|
54
|
+
);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const paths = await resolvePaths();
|
|
59
|
+
const result = await runPythonModule({
|
|
60
|
+
module: "okstra_ctl.report_finalize",
|
|
61
|
+
args: buildReportFinalizeArgs(args, paths),
|
|
62
|
+
stdio: "inherit-stdout",
|
|
63
|
+
});
|
|
64
|
+
return result.code;
|
|
65
|
+
}
|