okstra 0.133.0 → 0.135.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/architecture/storage-model.md +1 -1
- package/docs/cli.md +1 -1
- package/docs/for-ai/skills/okstra-pr-gen.md +2 -1
- package/docs/for-ai/skills/okstra-user-response.md +9 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +3 -1
- package/runtime/bin/okstra-antigravity-exec.sh +12 -2
- package/runtime/bin/okstra-claude-exec.sh +10 -1
- package/runtime/bin/okstra-codex-exec.sh +12 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/team-contract.md +2 -2
- package/runtime/prompts/profiles/_common-contract.md +0 -1
- package/runtime/prompts/profiles/_implementation-executor.md +20 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -0
- package/runtime/python/okstra_ctl/clarification_items.py +34 -12
- package/runtime/python/okstra_ctl/convergence_engine.py +11 -2
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +1 -1
- package/runtime/python/okstra_ctl/run.py +30 -11
- package/runtime/python/okstra_ctl/session.py +36 -7
- package/runtime/python/okstra_ctl/user_response.py +14 -5
- package/runtime/python/okstra_ctl/worker_artifact_paths.py +5 -1
- package/runtime/python/okstra_ctl/worker_heartbeat.py +35 -4
- package/runtime/python/okstra_ctl/worker_liveness.py +19 -6
- package/runtime/python/okstra_ctl/worktree.py +39 -9
- package/runtime/python/okstra_token_usage/collect.py +75 -9
- package/runtime/python/okstra_token_usage/pricing.py +4 -1
- package/runtime/skills/okstra-pr-gen/SKILL.md +24 -0
- package/runtime/skills/okstra-user-response/SKILL.md +67 -19
- package/runtime/validators/forbidden_actions.py +1 -1
- package/runtime/validators/validate-run.py +26 -8
- package/runtime/validators/validate_session_conformance.py +17 -7
|
@@ -96,19 +96,28 @@ def utc_now() -> str:
|
|
|
96
96
|
_CLI_WRAPPER_AGENTS = frozenset({"codex", "antigravity"})
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def
|
|
99
|
+
def _unavailable_usage_worker_labels(team_state: dict) -> frozenset[str]:
|
|
100
100
|
"""Token-table row labels whose `--` cells are honest.
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
Two ways a row legitimately renders `--`: a CLI-wrapper provider (tokens
|
|
103
|
+
live inside its own CLI) and any worker whose collected
|
|
104
|
+
`usage.source` is `unavailable`. The latter is not a value a worker typed —
|
|
105
|
+
the renderer (`okstra_token_usage/report.py` `_worker_detail_row`) emits
|
|
106
|
+
null cells for it, and the collector records *why* in the same block. Gating
|
|
107
|
+
it made an in-process worker the collector could not attribute fail the whole
|
|
108
|
+
run (observed dev-10172: no subagent jsonl carried the report-writer's
|
|
109
|
+
agentName, so a completed run was reported as contract-violated).
|
|
110
|
+
|
|
111
|
+
Labels mirror `_worker_detail_label` (`"{role} ({agent}, {status})"`) so the
|
|
112
|
+
match is on the rendered label the scanner actually sees.
|
|
105
113
|
"""
|
|
106
114
|
labels = set()
|
|
107
115
|
for worker in team_state.get("workers") or []:
|
|
108
116
|
if not isinstance(worker, dict):
|
|
109
117
|
continue
|
|
110
118
|
agent = str(worker.get("agent") or "").strip()
|
|
111
|
-
if
|
|
119
|
+
usage = worker.get("usage") if isinstance(worker.get("usage"), dict) else {}
|
|
120
|
+
if agent not in _CLI_WRAPPER_AGENTS and usage.get("source") != "unavailable":
|
|
112
121
|
continue
|
|
113
122
|
role = str(worker.get("role") or worker.get("workerId") or "Worker").strip()
|
|
114
123
|
status = str(worker.get("status") or "").strip()
|
|
@@ -3488,8 +3497,17 @@ def _validate_stage_carry_sidecar_exists(
|
|
|
3488
3497
|
stage = evidence.get("stageNumber")
|
|
3489
3498
|
if not isinstance(stage, int):
|
|
3490
3499
|
return
|
|
3491
|
-
#
|
|
3492
|
-
|
|
3500
|
+
# Carry sidecars are stage-SHARED: they live flat at
|
|
3501
|
+
# `runs/implementation/carry/stage-<N>.json`, NOT under the stage-<N>/ run
|
|
3502
|
+
# dir, because the next stage's carry-in and `consumers.backfill_done_from_carry`
|
|
3503
|
+
# glob them without knowing the producing run's layout (storage-model.md
|
|
3504
|
+
# §"representative files"). `report_path` is
|
|
3505
|
+
# `runs/implementation/stage-<N>/reports/final-report-...md`, so drop the
|
|
3506
|
+
# stage-<N>/ segment to reach the flat carry dir that `consumers` reads —
|
|
3507
|
+
# resolving it under the stage run dir forced the lead to write the file twice.
|
|
3508
|
+
run_dir = report_path.parent.parent
|
|
3509
|
+
carry_root = run_dir.parent if _stage_isolated_name(run_dir) else run_dir
|
|
3510
|
+
carry_path = carry_root / "carry" / f"stage-{stage}.json"
|
|
3493
3511
|
if not carry_path.exists():
|
|
3494
3512
|
failures.append(
|
|
3495
3513
|
f"implementation run declares stage-{stage} sidecar evidence but "
|
|
@@ -5778,7 +5796,7 @@ def main() -> int:
|
|
|
5778
5796
|
contract["required_agent_status_entries"],
|
|
5779
5797
|
failures,
|
|
5780
5798
|
allow_unavailable_token_usage=_session_accounting(team_state) == "artifact-only",
|
|
5781
|
-
unavailable_ok_labels=
|
|
5799
|
+
unavailable_ok_labels=_unavailable_usage_worker_labels(team_state),
|
|
5782
5800
|
)
|
|
5783
5801
|
validate_team_state_usage(team_state, failures)
|
|
5784
5802
|
|
|
@@ -36,7 +36,7 @@ for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "
|
|
|
36
36
|
|
|
37
37
|
from okstra_ctl.worker_heartbeat import ( # noqa: E402
|
|
38
38
|
HEARTBEAT_LINE_RE,
|
|
39
|
-
|
|
39
|
+
max_gap_seconds_after,
|
|
40
40
|
)
|
|
41
41
|
from okstra_ctl.wrapper_status import read_wrapper_status # noqa: E402
|
|
42
42
|
|
|
@@ -66,7 +66,9 @@ _PROGRESS_LINE_RE = re.compile(
|
|
|
66
66
|
# heartbeat 라인 shape 과 cadence 예산은 okstra_ctl.worker_heartbeat 정본을 쓴다 —
|
|
67
67
|
# `okstra worker-liveness` 가 run 도중 같은 판정을 내리므로 정의가 갈리면 안 된다.
|
|
68
68
|
_HEARTBEAT_LINE_RE = HEARTBEAT_LINE_RE
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
# 장기 단계 중 append 되는 진행 라인 (claude-worker.md "Heartbeat").
|
|
71
|
+
_IN_STAGE_PREFIX = "in-stage:"
|
|
70
72
|
|
|
71
73
|
# Phase 5/6 진입 전 lead 가 Read 해야 하는 implementation 프로파일 sidecar.
|
|
72
74
|
# 절대 경로는 레이어(repo / runtime / 설치본)마다 다르지만 basename 은 동일하다.
|
|
@@ -192,7 +194,7 @@ def _collect_lead_evidence(
|
|
|
192
194
|
)
|
|
193
195
|
from okstra_token_usage.paths import claude_project_dir
|
|
194
196
|
|
|
195
|
-
since, until = resolve_run_window(team_state_path, team_state)
|
|
197
|
+
since, until = resolve_run_window(team_state_path, team_state, relax_start=False)
|
|
196
198
|
lead_sid = (team_state.get("lead") or {}).get("sessionId") or ""
|
|
197
199
|
team_needles, _needle_source = resolve_team_needles_with_source(
|
|
198
200
|
team_state, project_root, since, until, projects_root=projects_root
|
|
@@ -546,7 +548,10 @@ def _check_heartbeat_sidecar(path: Path, worker_id: str, errors: list[str]) -> N
|
|
|
546
548
|
)
|
|
547
549
|
if worker_id == "report-writer":
|
|
548
550
|
for stage, _raw_ts in entries:
|
|
549
|
-
|
|
551
|
+
# `in-stage:<stage>` 는 계약이 장기 단계에 요구하는 진행 라인이다 —
|
|
552
|
+
# allowlist 가 이를 배제하면 워커는 cadence 를 지킬 방법이 없다.
|
|
553
|
+
base = stage[len(_IN_STAGE_PREFIX):] if stage.startswith(_IN_STAGE_PREFIX) else stage
|
|
554
|
+
if base not in _REPORT_WRITER_HEARTBEAT_STAGES:
|
|
550
555
|
errors.append(
|
|
551
556
|
f"`{rel}`: heartbeat stage `{stage}` is not allowed for report-writer."
|
|
552
557
|
)
|
|
@@ -560,6 +565,7 @@ def _check_heartbeat_sidecar(path: Path, worker_id: str, errors: list[str]) -> N
|
|
|
560
565
|
"stage must append its own line (agents/workers/claude-worker.md 'Heartbeat')."
|
|
561
566
|
)
|
|
562
567
|
prev: datetime | None = None
|
|
568
|
+
prev_stage = ""
|
|
563
569
|
for stage, raw_ts in entries:
|
|
564
570
|
ts = _parse_iso(raw_ts)
|
|
565
571
|
if ts is None:
|
|
@@ -569,18 +575,22 @@ def _check_heartbeat_sidecar(path: Path, worker_id: str, errors: list[str]) -> N
|
|
|
569
575
|
)
|
|
570
576
|
continue
|
|
571
577
|
if prev is not None:
|
|
578
|
+
# 예산은 구간을 여는 단계에서 고른다 — 이 공백이 재는 것은 직전에
|
|
579
|
+
# 선언된 단계의 작업 시간이다.
|
|
580
|
+
budget = max_gap_seconds_after(prev_stage)
|
|
572
581
|
gap = (ts - prev).total_seconds()
|
|
573
582
|
if gap < 0:
|
|
574
583
|
errors.append(
|
|
575
584
|
f"`{rel}`: heartbeat timestamps regress at stage `{stage}` ({raw_ts})."
|
|
576
585
|
)
|
|
577
|
-
elif gap >
|
|
586
|
+
elif gap > budget:
|
|
578
587
|
errors.append(
|
|
579
|
-
f"`{rel}`: heartbeat gap
|
|
580
|
-
"
|
|
588
|
+
f"`{rel}`: heartbeat gap after stage `{prev_stage}` is {int(gap)}s "
|
|
589
|
+
f"(budget {budget}s) — emit "
|
|
581
590
|
"`- PROGRESS: in-stage:<stage> <ISO>` during long stages."
|
|
582
591
|
)
|
|
583
592
|
prev = ts
|
|
593
|
+
prev_stage = stage
|
|
584
594
|
|
|
585
595
|
|
|
586
596
|
def _check_worker_liveness(
|