okstra 0.68.0 → 0.70.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/bin/okstra +18 -0
- package/docs/kr/architecture.md +1 -0
- package/docs/kr/cli.md +2 -1
- package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +526 -0
- package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +89 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +3 -3
- package/runtime/agents/workers/gemini-worker.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +2 -2
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -0
- package/runtime/prompts/profiles/_implementation-executor.md +3 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -0
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +8 -4
- package/runtime/python/okstra_ctl/conformance.py +17 -0
- package/runtime/python/okstra_ctl/paths.py +7 -4
- package/runtime/python/okstra_ctl/render.py +10 -3
- package/runtime/python/okstra_ctl/run.py +97 -20
- package/runtime/python/okstra_ctl/wizard.py +140 -38
- package/runtime/python/okstra_ctl/worktree.py +18 -0
- package/runtime/python/okstra_token_usage/collect.py +27 -0
- package/runtime/skills/okstra-convergence/SKILL.md +3 -3
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-report-writer/SKILL.md +6 -6
- package/runtime/skills/okstra-team-contract/SKILL.md +5 -5
- package/runtime/validators/validate-run.py +2 -2
- package/src/_python-helper.mjs +52 -0
- package/src/error-log.mjs +19 -0
- package/src/inject-report-index.mjs +22 -0
- package/src/render-final-report.mjs +22 -0
- package/src/render-views.mjs +9 -48
- package/src/spawn-followups.mjs +23 -0
- package/src/token-usage.mjs +3 -34
|
@@ -763,6 +763,70 @@ def _resolve_reuse_worktree(state: WizardState) -> bool:
|
|
|
763
763
|
return bool(entry and entry.status == "active")
|
|
764
764
|
|
|
765
765
|
|
|
766
|
+
def _base_ref_required(state: WizardState) -> bool:
|
|
767
|
+
return state.task_type != "final-verification" and state.reuse_worktree is False
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _base_ref_ready(state: WizardState) -> bool:
|
|
771
|
+
return not _base_ref_required(state) or S_BASE_REF_PICK in state.answered
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _branch_confirm_required(state: WizardState) -> bool:
|
|
775
|
+
return state.task_type != "final-verification"
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _stage_auto_allowed(state: WizardState) -> bool:
|
|
779
|
+
return state.task_type == "implementation"
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _parse_stage_objects(state: WizardState) -> list:
|
|
783
|
+
"""승인 plan 의 Stage Map stage 객체 목록. validator 의 _parse_stage_map 재사용.
|
|
784
|
+
`_build_stage_pick` 과 `_whole_task_allowed` 가 공유한다."""
|
|
785
|
+
import importlib.util as _ilu
|
|
786
|
+
import sys as _sys
|
|
787
|
+
plan_text = Path(state.approved_plan_path).read_text(encoding="utf-8")
|
|
788
|
+
validator_path = (Path(state.workspace_root) / "validators"
|
|
789
|
+
/ "validate-implementation-plan-stages.py")
|
|
790
|
+
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
791
|
+
if spec is None or spec.loader is None:
|
|
792
|
+
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
793
|
+
mod = _ilu.module_from_spec(spec)
|
|
794
|
+
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
795
|
+
try:
|
|
796
|
+
spec.loader.exec_module(mod)
|
|
797
|
+
stages, _errs = mod._parse_stage_map(plan_text)
|
|
798
|
+
finally:
|
|
799
|
+
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
800
|
+
return stages
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _done_stage_numbers(state: WizardState) -> set:
|
|
804
|
+
"""approved plan 을 소비한 implementation run 들의 consumers.jsonl 에서
|
|
805
|
+
done 처리된 stage 번호 집합. git 호출 없음 — 파일 읽기만(prepare 와 동일 SSOT)."""
|
|
806
|
+
if not state.approved_plan_path:
|
|
807
|
+
return set()
|
|
808
|
+
from .consumers import (read_consumers, backfill_done_from_carry,
|
|
809
|
+
latest_done_by_stage)
|
|
810
|
+
plan_run_root = Path(state.approved_plan_path).resolve().parents[1]
|
|
811
|
+
backfill_done_from_carry(plan_run_root)
|
|
812
|
+
rows = read_consumers(plan_run_root)
|
|
813
|
+
return set(latest_done_by_stage(rows).keys())
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _whole_task_allowed(state: WizardState) -> bool:
|
|
817
|
+
"""final-verification 이고 Stage Map 의 모든 stage 가 done 일 때만 True.
|
|
818
|
+
위저드는 done 만 본다 — 머지/clean/active 는 prepare 게이트가 강제한다."""
|
|
819
|
+
if state.task_type != "final-verification":
|
|
820
|
+
return False
|
|
821
|
+
if not state.approved_plan_path:
|
|
822
|
+
return False
|
|
823
|
+
stages = _parse_stage_objects(state)
|
|
824
|
+
if not stages:
|
|
825
|
+
return False
|
|
826
|
+
done = _done_stage_numbers(state)
|
|
827
|
+
return all(s.stage_number in done for s in stages)
|
|
828
|
+
|
|
829
|
+
|
|
766
830
|
def _existing_task_brief(project_root: Path, task_key: str) -> str:
|
|
767
831
|
"""Read taskBriefPath from manifest for an existing task. Empty if none."""
|
|
768
832
|
root = find_task_root(project_root, task_key)
|
|
@@ -1342,6 +1406,7 @@ _REUSE_LAST_TOKEN = "__reuse_last__"
|
|
|
1342
1406
|
_SIBLINGS_TOKEN = "__siblings__"
|
|
1343
1407
|
_LATEST_REPORT_TOKEN = "__latest_report__"
|
|
1344
1408
|
_PROJECT_DEFAULT_TOKEN = "__project_default__"
|
|
1409
|
+
WHOLE_TASK_STAGE = "__whole_task__"
|
|
1345
1410
|
|
|
1346
1411
|
|
|
1347
1412
|
def _list_implementation_planning_reports(
|
|
@@ -1477,39 +1542,35 @@ def _submit_approve_plan_confirm(state: WizardState, value: str) -> Optional[str
|
|
|
1477
1542
|
|
|
1478
1543
|
def _build_stage_pick(state: WizardState) -> Prompt:
|
|
1479
1544
|
"""Parse the Stage Map from the approved plan and build the stage picker."""
|
|
1480
|
-
import importlib.util as _ilu
|
|
1481
|
-
import sys as _sys
|
|
1482
1545
|
t = _p(state.workspace_root, "stage_pick")
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
spec = _ilu.spec_from_file_location("_ip_stage_v_wizard", str(validator_path))
|
|
1489
|
-
if spec is None or spec.loader is None:
|
|
1490
|
-
raise WizardError(f"cannot load stage validator at {validator_path}")
|
|
1491
|
-
mod = _ilu.module_from_spec(spec)
|
|
1492
|
-
_sys.modules["_ip_stage_v_wizard"] = mod
|
|
1493
|
-
try:
|
|
1494
|
-
spec.loader.exec_module(mod)
|
|
1495
|
-
stages, _errs = mod._parse_stage_map(plan_text)
|
|
1496
|
-
finally:
|
|
1497
|
-
_sys.modules.pop("_ip_stage_v_wizard", None)
|
|
1498
|
-
auto_label = (
|
|
1499
|
-
t["options"].get("auto_final_verification", t["options"]["auto"])
|
|
1500
|
-
if state.task_type == "final-verification"
|
|
1501
|
-
else t["options"]["auto"]
|
|
1546
|
+
stages = _parse_stage_objects(state)
|
|
1547
|
+
is_fv = state.task_type == "final-verification"
|
|
1548
|
+
label = (
|
|
1549
|
+
t.get("label_final_verification", t["label"])
|
|
1550
|
+
if is_fv else t["label"]
|
|
1502
1551
|
)
|
|
1503
|
-
|
|
1552
|
+
done = _done_stage_numbers(state) if is_fv else set()
|
|
1553
|
+
options = []
|
|
1554
|
+
if _stage_auto_allowed(state):
|
|
1555
|
+
options.append(_opt("auto", t["options"]["auto"]))
|
|
1556
|
+
if _whole_task_allowed(state):
|
|
1557
|
+
options.append(_opt(WHOLE_TASK_STAGE, t["options"]["whole_task"]))
|
|
1504
1558
|
for s in stages:
|
|
1505
1559
|
depends = ",".join(map(str, s.depends_on)) or "(none)"
|
|
1560
|
+
suffix = ""
|
|
1561
|
+
if is_fv:
|
|
1562
|
+
mark = (t["options"]["done_mark"]
|
|
1563
|
+
if s.stage_number in done
|
|
1564
|
+
else t["options"]["undone_mark"])
|
|
1565
|
+
suffix = f" {mark}"
|
|
1506
1566
|
options.append(_opt(
|
|
1507
1567
|
str(s.stage_number),
|
|
1508
|
-
f"{s.stage_number}: {s.title}
|
|
1568
|
+
f"{s.stage_number}: {s.title} "
|
|
1569
|
+
f"[depends-on: {depends} | steps: {s.step_count}]{suffix}",
|
|
1509
1570
|
))
|
|
1510
1571
|
return Prompt(
|
|
1511
1572
|
step=S_STAGE_PICK, kind="pick",
|
|
1512
|
-
label=
|
|
1573
|
+
label=label,
|
|
1513
1574
|
options=options,
|
|
1514
1575
|
echo_template=t["echo_template"],
|
|
1515
1576
|
)
|
|
@@ -1518,12 +1579,24 @@ def _build_stage_pick(state: WizardState) -> Prompt:
|
|
|
1518
1579
|
def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
|
|
1519
1580
|
if not answer:
|
|
1520
1581
|
raise WizardError("value required")
|
|
1521
|
-
if answer
|
|
1582
|
+
if answer == "auto":
|
|
1583
|
+
if not _stage_auto_allowed(state):
|
|
1584
|
+
raise WizardError(
|
|
1585
|
+
"final-verification requires an explicit stage number"
|
|
1586
|
+
)
|
|
1587
|
+
elif answer == WHOLE_TASK_STAGE:
|
|
1588
|
+
if not _whole_task_allowed(state):
|
|
1589
|
+
raise WizardError(
|
|
1590
|
+
"whole-task verification requires final-verification "
|
|
1591
|
+
"with all stages done"
|
|
1592
|
+
)
|
|
1593
|
+
else:
|
|
1522
1594
|
try:
|
|
1523
1595
|
int(answer)
|
|
1524
1596
|
except ValueError:
|
|
1525
1597
|
raise WizardError(
|
|
1526
|
-
f"answer must be 'auto' or a stage number,
|
|
1598
|
+
f"answer must be 'auto', whole-task, or a stage number, "
|
|
1599
|
+
f"got {answer!r}"
|
|
1527
1600
|
)
|
|
1528
1601
|
state.selected_stage = answer
|
|
1529
1602
|
return f"stage: {answer}"
|
|
@@ -2352,7 +2425,7 @@ STEPS: list[Step] = [
|
|
|
2352
2425
|
owns=("keep_existing_brief",)),
|
|
2353
2426
|
Step(S_BASE_REF_PICK,
|
|
2354
2427
|
applies=lambda s: (S_TASK_TYPE in s.answered
|
|
2355
|
-
and s
|
|
2428
|
+
and _base_ref_required(s)
|
|
2356
2429
|
and S_BASE_REF_PICK not in s.answered
|
|
2357
2430
|
and bool(s.brief_path)),
|
|
2358
2431
|
build=_build_base_ref_pick, submit=_submit_base_ref_pick,
|
|
@@ -2367,8 +2440,7 @@ STEPS: list[Step] = [
|
|
|
2367
2440
|
and not s.approved_plan_pending_text
|
|
2368
2441
|
and S_APPROVED_PLAN_PICK not in s.answered
|
|
2369
2442
|
and bool(s.brief_path)
|
|
2370
|
-
and (s
|
|
2371
|
-
or S_BASE_REF_PICK in s.answered)
|
|
2443
|
+
and _base_ref_ready(s)
|
|
2372
2444
|
and not s.base_ref_pending_text
|
|
2373
2445
|
and _latest_implementation_planning_report(s) is not None),
|
|
2374
2446
|
build=_build_approved_plan_pick, submit=_submit_approved_plan_pick,
|
|
@@ -2377,8 +2449,7 @@ STEPS: list[Step] = [
|
|
|
2377
2449
|
applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
|
|
2378
2450
|
and not s.approved_plan_path
|
|
2379
2451
|
and bool(s.brief_path)
|
|
2380
|
-
and (s
|
|
2381
|
-
or S_BASE_REF_PICK in s.answered)
|
|
2452
|
+
and _base_ref_ready(s)
|
|
2382
2453
|
and not s.base_ref_pending_text
|
|
2383
2454
|
and (s.approved_plan_pending_text
|
|
2384
2455
|
or _latest_implementation_planning_report(s) is None)),
|
|
@@ -2548,11 +2619,16 @@ STEPS: list[Step] = [
|
|
|
2548
2619
|
build=_build_pr_template_scope, submit=_submit_pr_template_scope,
|
|
2549
2620
|
owns=("pr_template_scope",)),
|
|
2550
2621
|
Step(S_BRANCH_CONFIRM,
|
|
2551
|
-
applies=lambda s: _ready_for_confirm(s)
|
|
2622
|
+
applies=lambda s: (_ready_for_confirm(s)
|
|
2623
|
+
and _branch_confirm_required(s)
|
|
2624
|
+
and s.branch_confirmed is None),
|
|
2552
2625
|
build=_build_branch_confirm, submit=_submit_branch_confirm,
|
|
2553
2626
|
owns=("branch_confirmed",)),
|
|
2554
2627
|
Step(S_CONFIRM,
|
|
2555
|
-
applies=lambda s: _ready_for_confirm(s)
|
|
2628
|
+
applies=lambda s: (_ready_for_confirm(s)
|
|
2629
|
+
and (not _branch_confirm_required(s)
|
|
2630
|
+
or s.branch_confirmed is True)
|
|
2631
|
+
and s.confirmed is None),
|
|
2556
2632
|
build=_build_confirm, submit=_submit_confirm,
|
|
2557
2633
|
owns=("confirmed", "edit_target")),
|
|
2558
2634
|
Step(S_EDIT_TARGET,
|
|
@@ -2570,7 +2646,7 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
2570
2646
|
return False
|
|
2571
2647
|
if not s.brief_path:
|
|
2572
2648
|
return False
|
|
2573
|
-
if s
|
|
2649
|
+
if _base_ref_required(s) and S_BASE_REF_PICK not in s.answered:
|
|
2574
2650
|
return False
|
|
2575
2651
|
if s.base_ref_pending_text:
|
|
2576
2652
|
return False
|
|
@@ -2763,7 +2839,24 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2763
2839
|
workers = state.workers_override.strip()
|
|
2764
2840
|
if state.task_type == "implementation":
|
|
2765
2841
|
workers = "" # profile-default roster is mandatory for impl
|
|
2766
|
-
base_ref =
|
|
2842
|
+
base_ref = (
|
|
2843
|
+
""
|
|
2844
|
+
if state.reuse_worktree or state.task_type == "final-verification"
|
|
2845
|
+
else state.base_ref
|
|
2846
|
+
)
|
|
2847
|
+
if state.task_type == "implementation":
|
|
2848
|
+
stage = state.selected_stage or "auto"
|
|
2849
|
+
elif state.task_type == "final-verification":
|
|
2850
|
+
if state.selected_stage == WHOLE_TASK_STAGE:
|
|
2851
|
+
stage = "" # prepare 가 빈 stage 를 whole-task 로 해석
|
|
2852
|
+
elif not state.selected_stage or state.selected_stage == "auto":
|
|
2853
|
+
raise WizardError(
|
|
2854
|
+
"final-verification requires an explicit stage number"
|
|
2855
|
+
)
|
|
2856
|
+
else:
|
|
2857
|
+
stage = state.selected_stage
|
|
2858
|
+
else:
|
|
2859
|
+
stage = ""
|
|
2767
2860
|
pr_template = (
|
|
2768
2861
|
state.pr_template_path
|
|
2769
2862
|
if state.task_type == "release-handoff"
|
|
@@ -2779,7 +2872,7 @@ def render_args(state: WizardState) -> dict[str, str]:
|
|
|
2779
2872
|
"executor": state.executor,
|
|
2780
2873
|
"critic": state.critic,
|
|
2781
2874
|
"approved-plan": state.approved_plan_path,
|
|
2782
|
-
"stage":
|
|
2875
|
+
"stage": stage,
|
|
2783
2876
|
"base-ref": base_ref,
|
|
2784
2877
|
"workers": workers,
|
|
2785
2878
|
"directive": state.directive,
|
|
@@ -2801,7 +2894,9 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2801
2894
|
lines.append(f" task-type : {state.task_type}")
|
|
2802
2895
|
lines.append(f" task-key : {state.task_group}/{state.task_id}")
|
|
2803
2896
|
lines.append(f" brief : {state.brief_path or '(none)'}")
|
|
2804
|
-
if state.
|
|
2897
|
+
if state.task_type == "final-verification":
|
|
2898
|
+
lines.append(" base-ref : (selected stage worktree)")
|
|
2899
|
+
elif state.reuse_worktree:
|
|
2805
2900
|
lines.append(" base-ref : (reusing existing worktree)")
|
|
2806
2901
|
else:
|
|
2807
2902
|
lines.append(f" base-ref : {state.base_ref}")
|
|
@@ -2829,7 +2924,14 @@ def confirmation_block(state: WizardState) -> str:
|
|
|
2829
2924
|
lines.append(f" critic : {state.critic or '(off)'}")
|
|
2830
2925
|
if state.task_type in _STAGE_SCOPED_TASK_TYPES:
|
|
2831
2926
|
lines.append(f" approved-plan : {state.approved_plan_path}")
|
|
2832
|
-
|
|
2927
|
+
stage = (
|
|
2928
|
+
_msg(state.workspace_root, "confirmation", "stage_whole_task")
|
|
2929
|
+
if state.selected_stage == WHOLE_TASK_STAGE
|
|
2930
|
+
else (state.selected_stage
|
|
2931
|
+
or ("auto" if state.task_type == "implementation"
|
|
2932
|
+
else "(not selected)"))
|
|
2933
|
+
)
|
|
2934
|
+
lines.append(f" stage : {stage}")
|
|
2833
2935
|
if state.clarification_response_path:
|
|
2834
2936
|
lines.append(f" clarification : {state.clarification_response_path}")
|
|
2835
2937
|
if state.task_type == "release-handoff" and state.pr_template_path:
|
|
@@ -373,6 +373,24 @@ def _resolve_snapshot_files(project_root: Optional[Path] = None) -> tuple[str, .
|
|
|
373
373
|
)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
def okstra_clean_gate_excludes(project_root: Optional[Path] = None) -> tuple[str, ...]:
|
|
377
|
+
"""Project-relative paths okstra owns and source clean gates should ignore."""
|
|
378
|
+
out: list[str] = []
|
|
379
|
+
seen: set[str] = set()
|
|
380
|
+
for rel in (
|
|
381
|
+
".okstra",
|
|
382
|
+
*_resolve_sync_dirs(project_root),
|
|
383
|
+
*_resolve_sync_files(project_root),
|
|
384
|
+
*_resolve_snapshot_files(project_root),
|
|
385
|
+
):
|
|
386
|
+
cleaned = rel.strip().removeprefix("./").rstrip("/")
|
|
387
|
+
if not cleaned or cleaned == "." or cleaned in seen:
|
|
388
|
+
continue
|
|
389
|
+
seen.add(cleaned)
|
|
390
|
+
out.append(cleaned)
|
|
391
|
+
return tuple(out)
|
|
392
|
+
|
|
393
|
+
|
|
376
394
|
def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
|
|
377
395
|
"""Symlink each configured dir from `source_root` (the MAIN
|
|
378
396
|
worktree) into the new worktree.
|
|
@@ -183,6 +183,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
183
183
|
# silently (observed in dev-9692 error-analysis: claude/codex workers
|
|
184
184
|
# dispatched without `name` → both unavailable, report-writer named → fine).
|
|
185
185
|
unattributed_sessions: list[str] = []
|
|
186
|
+
unattributed_totals: list[dict] = []
|
|
186
187
|
for sid, path in claude_sessions.items():
|
|
187
188
|
if sid == lead_sid:
|
|
188
189
|
lead_path = path
|
|
@@ -194,6 +195,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
194
195
|
by_agent.setdefault(agent, []).append((sid, path, totals))
|
|
195
196
|
else:
|
|
196
197
|
unattributed_sessions.append(sid)
|
|
198
|
+
unattributed_totals.append(totals)
|
|
197
199
|
|
|
198
200
|
# Lead.
|
|
199
201
|
if lead_path is not None:
|
|
@@ -273,6 +275,26 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
273
275
|
block["cliNote"] = f"{agent} CLI session found but no usage recorded (likely errored before completion)"
|
|
274
276
|
worker["usage"] = block
|
|
275
277
|
|
|
278
|
+
# Fold team-tagged worker sessions that carry no agentName into the worker
|
|
279
|
+
# pool. They cannot be mapped to a specific workerId (so each named worker
|
|
280
|
+
# row above stays `unavailable`), but the tokens are real team-worker spend —
|
|
281
|
+
# most often an in-process teammate whose work is commingled in a team-tagged
|
|
282
|
+
# session the harness never tagged with `name`. Without this, the run-level
|
|
283
|
+
# Worker total reads 0 and the report validator hard-fails a legitimate run.
|
|
284
|
+
# Attribution is aggregate, not per-worker; usageSummary records it openly.
|
|
285
|
+
unattributed_usage = None
|
|
286
|
+
if unattributed_totals:
|
|
287
|
+
unattributed_usage = usage_block(
|
|
288
|
+
_aggregate_totals(unattributed_totals), source="claude-jsonl"
|
|
289
|
+
)
|
|
290
|
+
unattributed_usage["sessionIds"] = unattributed_sessions
|
|
291
|
+
unattributed_usage["note"] = (
|
|
292
|
+
"Team-tagged worker session(s) with no agentName (dispatched without "
|
|
293
|
+
"the Agent `name` arg, or an in-process teammate commingled with the "
|
|
294
|
+
"lead). Folded into the worker pool as an aggregate because they "
|
|
295
|
+
"cannot be mapped to a specific workerId."
|
|
296
|
+
)
|
|
297
|
+
|
|
276
298
|
# Aggregate summary.
|
|
277
299
|
lead = state.get("leadUsage") or {}
|
|
278
300
|
workers = state.get("workers", [])
|
|
@@ -283,6 +305,10 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
283
305
|
worker_billable = sum((w.get("usage") or {}).get("billableEquivalentTokens", 0) or 0 for w in workers)
|
|
284
306
|
worker_cost = sum((w.get("usage") or {}).get("estimatedCostUsd", 0) or 0 for w in workers)
|
|
285
307
|
cli_cost = sum((w.get("usage") or {}).get("cliEstimatedCostUsd", 0) or 0 for w in workers)
|
|
308
|
+
if unattributed_usage is not None:
|
|
309
|
+
worker_total += unattributed_usage.get("totalTokens", 0) or 0
|
|
310
|
+
worker_billable += unattributed_usage.get("billableEquivalentTokens", 0) or 0
|
|
311
|
+
worker_cost += unattributed_usage.get("estimatedCostUsd", 0) or 0
|
|
286
312
|
|
|
287
313
|
# Surface models whose pricing lookup failed so the silent-zero case is visible.
|
|
288
314
|
unmatched_models: list[str] = []
|
|
@@ -312,6 +338,7 @@ def collect(team_state_path: Path, project_root: Path | None = None, *,
|
|
|
312
338
|
"sessionsFound": len(claude_sessions),
|
|
313
339
|
"unmatchedModels": sorted(set(unmatched_models)),
|
|
314
340
|
"unattributedTeamSessions": unattributed_sessions,
|
|
341
|
+
"unattributedWorkerUsage": unattributed_usage,
|
|
315
342
|
"definitions": {
|
|
316
343
|
"totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
|
|
317
344
|
"billableEquivalentTokens": "Tokens normalized to base-input-price units (cache_creation_5m x1.25, cache_creation_1h x2.0, cache_read x0.1, output x5). 5m vs 1h is split from usage.cache_creation when the API breakdown is present; otherwise all cache_creation falls into 5m.",
|
|
@@ -169,7 +169,7 @@ A reverify dispatch that returns a **terminal non-result** (`timeout`, `error`,
|
|
|
169
169
|
Rules:
|
|
170
170
|
|
|
171
171
|
1. For each affected finding, append a `votes[W].verdict = "verification-error"` entry instead of `disagree`, plus the wrapper's captured exit reason in `votes[W].explanation`.
|
|
172
|
-
2. Record one event per failed dispatch via `
|
|
172
|
+
2. Record one event per failed dispatch via `okstra error-log append-observed --error-type cli-failure --agent <worker> ...` (the worker wrapper does this for Codex/Gemini; for Claude worker timeouts the lead does it).
|
|
173
173
|
3. Add an entry to the round's `skippedWorkers[]` with `{worker: <W>, reason: "dispatch-non-result", terminalStatus: <timeout|error|not-run>}`.
|
|
174
174
|
4. If at least one dispatch was issued AND all reverify dispatches in a round terminate as non-result (mirroring the pseudocode's `len(dispatches) > 0` guard), the round is treated as gate-closed: write `round2SkippedReason: "all-reverify-non-result"` (even if the round in question is round 1 — i.e. round 2 never runs because round 1 produced no usable votes), record one `contract-violation` event per non-result dispatch, and exit the WHILE loop.
|
|
175
175
|
5. Section 6 (Specialization Lens) of a worker output is OUT of convergence scope per "Convergence scope" above — its absence is NEVER a `verification-error`.
|
|
@@ -293,7 +293,7 @@ Assigned worker prompt history path: <Project Root>/<Prompt History Path>
|
|
|
293
293
|
2. `team-state-<task-type>-<seq>.json` → `workers[].usage.cliModel` for that role (initial run's actual execution value)
|
|
294
294
|
3. The `**Model:**` line of the initial Phase 4 prompt for that role (read from its persisted prompt-history file)
|
|
295
295
|
|
|
296
|
-
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra
|
|
296
|
+
If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess, do NOT fall back to training-data defaults — for codex this would silently produce `o4-mini` instead of the assigned `gpt-5.5`-class model, which is a real bug class observed in production.
|
|
297
297
|
|
|
298
298
|
For Codex/Gemini wrapper subagents, the `**Model:** <role>, <modelExecutionValue>` line is what their wrapper extracts to pass into the underlying CLI's `--model` flag. Omitting it forces the wrapper to fall back to its own training-data knowledge of the CLI's historical default.
|
|
299
299
|
|
|
@@ -572,7 +572,7 @@ Promoted blockers enter `## 5.8 Acceptance Blockers`; since `accepted` requires
|
|
|
572
572
|
|
|
573
573
|
### State
|
|
574
574
|
|
|
575
|
-
Critic output lives
|
|
575
|
+
Critic output lives in the run's `worker-results/` directory (`runs/final-verification/worker-results/` for whole-task verification, `runs/final-verification/stage-<N>/worker-results/` for single-stage), filename `<provider>-critic-final-verification-<seq>.md`. The convergence state `config.critic` summary (see §"Coverage critic pass") records `mode: "acceptance-devils-advocate"`, `candidatesProposed`, `confirmedBlockers`, `downgradedToResidual` (optional v1.2 fields; readers treat absence as null).
|
|
576
576
|
|
|
577
577
|
## Output
|
|
578
578
|
|
|
@@ -299,7 +299,7 @@ B. **`task-manifest.json` (direct):** if catalog missing, slugify task-group / t
|
|
|
299
299
|
|
|
300
300
|
C. **`timeline.json` (specific run):** for a specific date or run, read `.okstra/tasks/<group-segment>/<id-segment>/history/timeline.json`, filter `runs[]` by `runTimestamp` / `status` / `taskType`, use `runs[].reportPath`.
|
|
301
301
|
|
|
302
|
-
D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.md` per `scripts/okstra_ctl/sequence.py:31`. Highest seq is latest. Cross-verify with `timeline.json`'s `runs[].taskType` filter.
|
|
302
|
+
D. **Specific task-type (fallback):** `latestReportPath` is task-type-agnostic. For a specific task-type's latest report, look under `.okstra/tasks/<group-segment>/<id-segment>/runs/<task-type-segment>/reports/`, filename pattern `final-report-<task-type-segment>-<NNN>.md` per `scripts/okstra_ctl/sequence.py:31`. Highest seq is latest. Stage-isolated runs (`implementation`, single-stage `final-verification`) keep their reports one level deeper at `runs/<task-type-segment>/stage-<N>/reports/` — scan those subdirectories too; seq is independent per stage. Cross-verify with `timeline.json`'s `runs[].taskType` filter.
|
|
303
303
|
|
|
304
304
|
### report.2 — Confirm existence
|
|
305
305
|
|
|
@@ -60,7 +60,7 @@ The prompt MUST include, in this order at the top:
|
|
|
60
60
|
before the dispatch is constructed. The worker copies this verbatim
|
|
61
61
|
into `data.json.meta.reportLanguage`.
|
|
62
62
|
11. For implementation-planning runs: a literal block listing the 8 required English section headings the validator scans for (`Option Candidates`, `Trade-off`, `Recommended Option`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `User Approval Request`). The writer must use these exact substrings as section headings (Korean translation in parentheses is allowed).
|
|
63
|
-
12. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "
|
|
63
|
+
12. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" via Bash so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
|
|
64
64
|
|
|
65
65
|
**Completion detection after dispatch (BLOCKING).** The `Agent(... team_name ...)` call returns `Spawned successfully` immediately; that ack is NOT completion. After dispatching the report-writer (async), Lead MUST detect its completion via the self-scheduled polling protocol in [okstra-team-contract](../okstra-team-contract/SKILL.md) "Worker-completion detection (self-scheduled polling)", polling for the appearance of the data.json (Result Path) and the worker-results file (Worker Result Path) — do NOT restate the algorithm here. Report-writer is a single worker, so the pending set has one entry; the SSOT protocol handles that naturally. Lead MUST NOT treat the `Spawned successfully` ack as completion and MUST NOT end its turn with a prose "waiting for the report" statement; that path stalls the run until the user manually nudges it.
|
|
66
66
|
|
|
@@ -78,7 +78,7 @@ Except for `release-handoff` (which is single-lead by design and never dispatche
|
|
|
78
78
|
|
|
79
79
|
1. A Report writer worker dispatch was actually attempted (Agent call was issued).
|
|
80
80
|
2. The attempt recorded a terminal status of `error`, `timeout`, or `not-run` with a concrete reason (tool error message, timeout duration, or external blocker).
|
|
81
|
-
3. The reason is logged via `okstra
|
|
81
|
+
3. The reason is logged via `okstra error-log append-observed --error-type cli-failure ...` (or `tool-failure` if the failure was internal).
|
|
82
82
|
|
|
83
83
|
Speculative reasons such as "session resume constraint", "team object no longer exists", or "lead can do it faster" are NOT valid.
|
|
84
84
|
|
|
@@ -90,7 +90,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
90
90
|
2. **Phase 7 step 1 — Token-usage collector with `--substitute-data`** (BLOCKING). One invocation aggregates `leadUsage` / `workers[].usage` / `usageSummary` into team-state AND populates `tokenUsage` + `executionStatus[].totalTokens` etc. in the data.json AND re-invokes the renderer so the sibling markdown carries the real numbers. Skipping the flag ships a markdown full of `--` cells.
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
|
|
93
|
+
okstra token-usage \
|
|
94
94
|
<runDirectoryPath>/state/team-state-<task-type>-<seq>.json \
|
|
95
95
|
--write --summary \
|
|
96
96
|
--substitute-data <runDirectoryPath>/reports/final-report-<task-type>-<seq>.data.json
|
|
@@ -100,7 +100,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
100
100
|
3. **Phase 7 step 1.5 — Render report views** (BLOCKING, conditional output). Always invoke the renderer; it decides whether an html sibling is warranted:
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
|
-
|
|
103
|
+
okstra render-views \
|
|
104
104
|
<runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
|
|
105
105
|
```
|
|
106
106
|
|
|
@@ -112,7 +112,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
|
|
|
112
112
|
4. **Phase 7 step 2 — Follow-up task spawner** (BLOCKING when Section 4 is non-empty). Turns the report's `## 4. Follow-up Tasks (후속 작업)` rows into `tasks/<task-group>/<new-task-id>/` stubs.
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
|
|
115
|
+
okstra spawn-followups \
|
|
116
116
|
<runDirectoryPath>/reports/final-report-<task-type>-<seq>.data.json \
|
|
117
117
|
--project-root <project_root> \
|
|
118
118
|
--task-group <task-group> \
|
|
@@ -323,7 +323,7 @@ Persistence steps that must be performed in Phase 7:
|
|
|
323
323
|
- [ ] 5. **Update task-index.md**: Refresh human-readable summary
|
|
324
324
|
- [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)
|
|
325
325
|
- [ ] 7. **Save convergence state**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (when convergence is enabled)
|
|
326
|
-
- [ ] 8. **Spawn follow-up task stubs**: run `
|
|
326
|
+
- [ ] 8. **Spawn follow-up task stubs**: run `okstra spawn-followups` against the final-report per the canonical spawn rule defined in "Phase 7 follow-up task spawner" above. Do not restate the trigger condition here — that section is the single source of truth. The script is idempotent across reruns.
|
|
327
327
|
- [ ] 9. **Human HTML report** (conditional): `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — produced by Phase 7 step 1.5 **only when the report has ≥1 §5 `C-*` clarification row** (self-contained, embeds `Export user response` button). Clarification-free reports legitimately have no html sibling; do not treat its absence as a missing artifact.
|
|
328
328
|
|
|
329
329
|
### Response after Persistence
|
|
@@ -303,7 +303,7 @@ Schema:
|
|
|
303
303
|
Workers MUST omit `source` / `recordedAt` / `agent` / `agentRole` / `model` /
|
|
304
304
|
`taskKey`. Claude lead fills those in when dumping the sidecar to the
|
|
305
305
|
run-level errors log (`runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`)
|
|
306
|
-
via `
|
|
306
|
+
via `okstra error-log append-from-worker`.
|
|
307
307
|
|
|
308
308
|
Workers MUST use only `errorType: "tool-failure"` in the **sidecar file**.
|
|
309
309
|
|
|
@@ -312,7 +312,7 @@ run-level errors log path or their sidecar path from the
|
|
|
312
312
|
`runs/<task-type>/...` template syntax. Both absolute paths are delivered
|
|
313
313
|
by Lead via two dispatch-prompt header lines:
|
|
314
314
|
|
|
315
|
-
- `**Errors log path:** <absolute path>` — run-level JSONL (`okstra
|
|
315
|
+
- `**Errors log path:** <absolute path>` — run-level JSONL (`okstra error-log append-observed --out ...`)
|
|
316
316
|
- `**Errors sidecar path:** <absolute path>` — per-worker JSON (`{ "schemaVersion": 1, "errors": [...] }`)
|
|
317
317
|
|
|
318
318
|
Lead obtains both paths from the launch prompt's `## Run Logs (error-log
|
|
@@ -322,7 +322,7 @@ without proceeding — this is the contractual replacement for the previous
|
|
|
322
322
|
"derive from template placeholders" behavior, which silently produced
|
|
323
323
|
empty run-level error logs in production.
|
|
324
324
|
|
|
325
|
-
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra
|
|
325
|
+
- `cli-failure` events are recorded by the wrapper subagent itself (Codex / Gemini), but **directly to the run-level error log** via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
|
|
326
326
|
- **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-gemini-exec.sh` accept four required positional arguments plus an optional fifth `<role>`: `<project-root> <model> <prompt-path> <worktree-path> [<role>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for gemini it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>-<pid>` and the sibling trace-pane title `<cli>-<role>-<pid>-trace[from=<caller-pane-id>]` (e.g. `codex-worker-93421` ↔ `codex-worker-93421-trace[from=%5]`). `<pid>` is the wrapper's own PID and disambiguates concurrent dispatches of the same role; the embedded caller pane id keeps the trace ↔ worker correlation visible even when the worker pane's title is overwritten by the parent process (Claude Code's TUI emits OSC 2 title escape sequences on its own pane). Always pass the literal string `worker` so the dispatch is self-describing (the wrapper defaults to `worker` if omitted).
|
|
327
327
|
- **Background dispatch + polling contract (Codex / Gemini wrappers).** Both wrapper subagents MUST dispatch `okstra-codex-exec.sh` / `okstra-gemini-exec.sh` via `Bash(run_in_background: true)` and poll with `BashOutput(bash_id)` until the shell reports `status == "completed"`, capped at 30 minutes (1800s) of wall-clock elapsed time. `BashOutput` itself is the wait primitive — call it back-to-back; do NOT insert a standalone `sleep` between polls. The Claude Code harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps inside until-loops to work around the block. Workers that hit the contract bug must NOT self-recover with `until ...; do sleep 2; done` wrappers — that path violates the harness anti-circumvention rule, even though it superficially "works". The legacy "single foreground `Bash` with 120000ms timeout" rule, and the subsequent "60-second cadence with `sleep 60` between polls" rule, are both retired. The current rule applies in **every phase** (analysis runs typically complete in 1–2 `BashOutput` calls, so there is no regression for short jobs). Recording responsibilities:
|
|
328
328
|
- Successful completion: return the wrapper's accumulated stdout from the final `BashOutput`. No log entry.
|
|
@@ -330,8 +330,8 @@ empty run-level error logs in production.
|
|
|
330
330
|
- Polling cap reached: before `KillShell`, perform a one-shot **mtime-grace check** on the wrapper's live log (`<prompt>.log`). If the log was written within the last 90 seconds AND grace has not yet been applied this loop, extend the cap from 1800s → 2100s (one-shot +5min) and continue polling. Otherwise (log stale, OR grace already applied), call `KillShell(shell_id)`, record `cli-failure` with `--exit-code 124 --duration-ms <observed_ms> --message "<wrapper> exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, then return the language-specific `*_CLI_TIMEOUT` sentinel. The grace exists to absorb token-budget spikes where the CLI is genuinely still producing output past the 30-minute mark; it is a one-shot soft extension, NOT a loop.
|
|
331
331
|
- Token-usage matching is unaffected: the wrapper subagent stays alive throughout polling, so the wrapper's jsonl timestamp window continues to cover the underlying CLI rollout's full duration (see §"Token-usage accounting" below).
|
|
332
332
|
- **No external timeout on wrapper subagents.** The codex/gemini wrapper subagent's polling loop (with optional mtime grace) is the SINGLE timeout authority for its dispatch. Lead MUST NOT impose a separate `Agent()` call timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before its own polling cap is reached. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The wrapper's polling cap (30min + optional 5min grace) is calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, lower it in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
|
|
333
|
-
- `contract-violation` events (C) are recorded by Lead via `okstra
|
|
334
|
-
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra
|
|
333
|
+
- `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
|
|
334
|
+
- Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
|
|
335
335
|
|
|
336
336
|
## Convergence Phase Rules
|
|
337
337
|
|
|
@@ -621,7 +621,7 @@ def _scan_token_usage_summary(content: str, failures: list[str]) -> None:
|
|
|
621
621
|
failures.append(
|
|
622
622
|
f"Token Usage Summary row `{label_cell or '<unlabeled>'}` has "
|
|
623
623
|
f"a zero value `{stripped}` — no okstra run consumes zero "
|
|
624
|
-
"tokens. Re-run `
|
|
624
|
+
"tokens. Re-run `okstra token-usage "
|
|
625
625
|
"<team-state> --write --summary --substitute-data "
|
|
626
626
|
"<report-path>` to repopulate from session jsonls. The "
|
|
627
627
|
"Codex/Gemini CLI row is the only place `$0.00` is "
|
|
@@ -1024,7 +1024,7 @@ def validate_team_state_usage(team_state: dict, failures: list[str]) -> None:
|
|
|
1024
1024
|
if not summary or not summary.get("collectedAt"):
|
|
1025
1025
|
failures.append(
|
|
1026
1026
|
"team-state.usageSummary is empty — Phase 7 token-usage collection was skipped. "
|
|
1027
|
-
"Run `
|
|
1027
|
+
"Run `okstra token-usage <team-state> --write --summary "
|
|
1028
1028
|
"--substitute-data <final-report>`."
|
|
1029
1029
|
)
|
|
1030
1030
|
return
|
package/src/_python-helper.mjs
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
2
5
|
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
3
6
|
|
|
7
|
+
function resolveInstalledScript(paths, scriptName) {
|
|
8
|
+
// Prefer the installed copy under ~/.okstra/bin (what production users run);
|
|
9
|
+
// fall back to the in-repo source when invoked from a checkout that has not
|
|
10
|
+
// been installed (dev / CI).
|
|
11
|
+
const installed = join(paths.bin, scriptName);
|
|
12
|
+
if (existsSync(installed)) return installed;
|
|
13
|
+
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
14
|
+
const dev = resolvePath(repoRoot, "scripts", scriptName);
|
|
15
|
+
return existsSync(dev) ? dev : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Thin spawn shim shared by every `okstra <cmd>` subcommand that fronts a
|
|
19
|
+
// `scripts/okstra-*.py` entry point. Centralizing it keeps PYTHONPATH wiring
|
|
20
|
+
// and installed/dev resolution in one place so skills call `okstra <cmd>`
|
|
21
|
+
// instead of emitting `python3 "$HOME/..."` (which breaks `Bash(okstra:*)`
|
|
22
|
+
// permission matching and prompts on every call).
|
|
23
|
+
export async function runInstalledScript({ scriptName, args, usage, emptyArgsCode = 2 }) {
|
|
24
|
+
if (args.length === 0) {
|
|
25
|
+
process.stdout.write(usage);
|
|
26
|
+
return emptyArgsCode;
|
|
27
|
+
}
|
|
28
|
+
// Only a bare `--help` / `-h` prints the wrapper usage. A `--help` that
|
|
29
|
+
// follows a subcommand (e.g. `error-log append-observed --help`) must reach
|
|
30
|
+
// the python helper so its own per-subcommand help shows through.
|
|
31
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
32
|
+
process.stdout.write(usage);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const paths = await resolvePaths();
|
|
36
|
+
const entry = resolveInstalledScript(paths, scriptName);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`error: ${scriptName} not found — run 'okstra install' (or 'okstra ensure-installed') first\n`,
|
|
40
|
+
);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return await new Promise((resolve) => {
|
|
44
|
+
const child = spawn("python3", [entry, ...args], {
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
env: { ...process.env, PYTHONPATH: buildPythonpath(paths) },
|
|
47
|
+
});
|
|
48
|
+
child.on("error", (err) => {
|
|
49
|
+
process.stderr.write(`error: failed to spawn python3: ${err.message}\n`);
|
|
50
|
+
resolve(1);
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => resolve(typeof code === "number" ? code : 1));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
4
56
|
export async function runPythonSnippet({ script, args = [], extraEnv = {} }) {
|
|
5
57
|
const paths = await resolvePaths();
|
|
6
58
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra error-log — append okstra run error events to the run error log
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-error-log.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so skills and worker wrappers call \`okstra error-log\`
|
|
7
|
+
instead of emitting a \`python3 "$HOME/..."\` invocation (which breaks
|
|
8
|
+
\`Bash(okstra:*)\` permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra error-log <subcommand> [...] # e.g. append-observed / append-from-worker
|
|
12
|
+
|
|
13
|
+
All arguments are forwarded verbatim to the python helper. See
|
|
14
|
+
\`okstra error-log append-observed --help\` for the full option list.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export async function run(args) {
|
|
18
|
+
return runInstalledScript({ scriptName: "okstra-error-log.py", args, usage: USAGE });
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { runInstalledScript } from "./_python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra inject-report-index — add the top-of-report Index + scroll anchors to a report
|
|
4
|
+
|
|
5
|
+
Wraps the python helper (\`okstra-inject-report-index.py\`) installed under
|
|
6
|
+
\`~/.okstra/bin/\` so skills call \`okstra inject-report-index\` instead of
|
|
7
|
+
emitting a \`python3 "$HOME/..."\` invocation (which breaks \`Bash(okstra:*)\`
|
|
8
|
+
permission matching and prompts on every call).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
okstra inject-report-index <markdown-path> [--report-language <en|ko>]
|
|
12
|
+
|
|
13
|
+
All arguments are forwarded verbatim to the python helper.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function run(args) {
|
|
17
|
+
return runInstalledScript({
|
|
18
|
+
scriptName: "okstra-inject-report-index.py",
|
|
19
|
+
args,
|
|
20
|
+
usage: USAGE,
|
|
21
|
+
});
|
|
22
|
+
}
|