okstra 0.125.6 → 0.128.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 +10 -3
- package/docs/architecture.md +14 -2
- package/docs/cli.md +1 -0
- package/docs/project-structure-overview.md +9 -6
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/okstra-antigravity-exec.sh +8 -2
- package/runtime/bin/okstra-claude-exec.sh +8 -2
- package/runtime/bin/okstra-codex-exec.sh +65 -8
- package/runtime/prompts/lead/adapters/claude-code.md +1 -0
- package/runtime/prompts/lead/convergence.md +8 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +26 -1
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +14 -3
- package/runtime/prompts/profiles/final-verification.md +10 -6
- package/runtime/python/okstra_ctl/analysis_packet.py +7 -2
- package/runtime/python/okstra_ctl/codex_dispatch.py +27 -1
- package/runtime/python/okstra_ctl/design_surfaces.py +30 -14
- package/runtime/python/okstra_ctl/dispatch_core.py +20 -0
- package/runtime/python/okstra_ctl/log_report.py +44 -5
- package/runtime/python/okstra_ctl/path_hints.py +5 -0
- package/runtime/python/okstra_ctl/render.py +22 -0
- package/runtime/python/okstra_ctl/render_final_report.py +71 -5
- package/runtime/python/okstra_ctl/run.py +69 -3
- package/runtime/python/okstra_ctl/schema_excerpt.py +17 -1
- package/runtime/python/okstra_ctl/wizard.py +15 -4
- package/runtime/python/okstra_ctl/work_categories.py +37 -0
- package/runtime/python/okstra_ctl/worker_heartbeat.py +50 -0
- package/runtime/python/okstra_ctl/worker_liveness.py +140 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +223 -0
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +51 -2
- package/runtime/templates/reports/final-report.template.md +9 -9
- package/runtime/templates/worker-prompt-preamble.md +13 -5
- package/runtime/validators/forbidden_actions.py +43 -4
- package/runtime/validators/validate-implementation-plan-stages.py +6 -4
- package/runtime/validators/validate-report-views.py +4 -4
- package/runtime/validators/validate-run.py +61 -6
- package/runtime/validators/validate-schedule.py +6 -4
- package/runtime/validators/validate_fanout.py +4 -3
- package/runtime/validators/validate_improvement_report.py +6 -4
- package/runtime/validators/validate_session_conformance.py +16 -7
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/log-report.mjs +5 -3
- package/src/commands/inspect/worker-liveness.mjs +31 -0
- package/src/commands/lifecycle/preflight.mjs +25 -2
- package/src/lib/helper-scripts.mjs +23 -0
- package/src/lib/python-helper.mjs +1 -1
|
@@ -91,29 +91,37 @@ If a required header line is absent from the dispatch prompt, return `<SENTINEL_
|
|
|
91
91
|
|
|
92
92
|
## Anchor headers (lead-injected, BLOCKING)
|
|
93
93
|
|
|
94
|
-
Every worker prompt MUST begin with
|
|
94
|
+
Every initial worker prompt MUST begin with the common anchor headers below in this exact order, before any other content. An `implementation` prompt inserts the implementation-only coding-preflight anchor at the documented position. Workers extract the anchors verbatim.
|
|
95
95
|
|
|
96
96
|
1. `**Project Root:** <absolute-path>` — required so the worker can self-anchor without relying on inherited cwd.
|
|
97
97
|
2. `**Prompt History Path:** <project-relative-path>`
|
|
98
98
|
3. `**Result Path:** <project-relative-path>` — canonical destination for the worker's result file.
|
|
99
99
|
4. `Assigned worker prompt history path: <absolute-path>` — same as the prompt-history path but resolved against `Project Root`. Codex / Antigravity wrapper subagents extract this exact line.
|
|
100
100
|
5. `**Worker Preamble Path:** <absolute-path>` — points to THIS file. Workers Read it end-to-end before doing anything else.
|
|
101
|
-
6. `**Coding preflight pack:** <absolute-path>` — points to the installed runtime resource pack under `<OKSTRA_HOME>/prompts/coding-preflight`. Implementation executors and verifiers use
|
|
101
|
+
6. `**Coding preflight pack:** <absolute-path>` — implementation-only; points to the installed runtime resource pack under `<OKSTRA_HOME>/prompts/coding-preflight`. It is present only when `taskType == implementation` and is absent from every non-implementation initial prompt. Implementation executors and implementation verifiers use it to read `overview.md`, `clean-code.md`, and routed language/framework/architecture resources. A pane display role named `verifier` does not activate this anchor in `final-verification`.
|
|
102
102
|
7. `**Errors log path:** <absolute-path>` — run-level JSONL (see Error reporting above).
|
|
103
103
|
8. `**Errors sidecar path:** <absolute-path>` — per-worker JSON (see Error reporting above).
|
|
104
|
+
9. `**Read scope:** …` — restates the *Reading rules* allowlist inline. This file is anchor 5, a path the worker opens partway into its run; by then a host SessionStart hook or a global `CLAUDE.md` / `AGENTS.md` project-conditional has already pointed it at `graphify-out/`, skill catalogs, and similar non-okstra artifacts. Only the inline copy arrives before the worker's first tool call.
|
|
104
105
|
|
|
105
|
-
**Exception — reverify dispatches.** A Phase 5.5 re-verification prompt (its `**Prompt History Path:**` carries a `-reverify-r<N>-` segment) deliberately omits
|
|
106
|
+
**Exception — reverify dispatches.** A Phase 5.5 re-verification prompt (its `**Prompt History Path:**` carries a `-reverify-r<N>-` segment) deliberately omits anchor 5 and adds a `**Model:**` line — lightweight mode reads neither this file nor the implementation-only preflight pack. The two errors-path anchors are NOT omitted; that gate applies to reverify prompts unchanged. The reverify anchor set is owned by `prompts/lead/convergence.md` "Required reverify-prompt anchor headers".
|
|
106
107
|
|
|
107
108
|
For the **implementation phase** specifically, the dispatched prompt MUST also include:
|
|
108
109
|
|
|
109
110
|
- `**Worktree:** <absolute-path>` — the task worktree path.
|
|
110
111
|
- `cwd for every mutating command: <absolute-path>` — same as Worktree path; used by codex / antigravity wrappers as `--add-dir` / `--include-directories`.
|
|
111
112
|
|
|
112
|
-
For the **final-verification phase** specifically, the dispatched prompt MUST
|
|
113
|
+
For the **final-verification phase** specifically, the dispatched prompt MUST include the compact identity of the canonical verification-target sidecar so every analyser verifies the SAME target the lead captured at the entry gate:
|
|
113
114
|
|
|
114
115
|
- `**Worktree:** <absolute-path>` — the checkout under verification (read-only).
|
|
116
|
+
- `**Verification scope:** <whole-task|single-stage>` — the selected verification mode.
|
|
115
117
|
- `**Verification base ref:** <base-ref>` — the implementation base for the diff.
|
|
116
|
-
- `**Verification
|
|
118
|
+
- `**Verification head ref:** <head-ref>` — the exact delivered revision under verification.
|
|
119
|
+
- `**Verification target path:** <project-relative-path>` — the canonical `instruction-set/verification-target.md` sidecar containing stages, source reports, and the complete captured diff stat.
|
|
120
|
+
- `**Verification target digest:** sha256:<digest>` — the digest over the normalized snapshot body.
|
|
121
|
+
|
|
122
|
+
Read the sidecar on demand when the full stage/report mapping or diff stat is needed. Do not copy its `Verification diff stat` block into the dispatch prompt.
|
|
123
|
+
|
|
124
|
+
The initial analysis prompt may contain exactly one `## Run-specific directive` section only when the run has a shared delta not already captured by the packet. The section applies unchanged to every selected analysis worker and is limited to 40 nonblank lines. If it would be longer, the lead writes the shared material into the instruction set and adds its common reference to `analysis-packet.md`; workers must not accept provider-, model-, or worker-specific verification scopes.
|
|
117
125
|
|
|
118
126
|
For the **improvement-discovery phase** specifically, the dispatched prompt MUST also include the resolved-scope log the lead wrote during Phase 1.5, so workers key off an explicit absolute path instead of re-deriving the run directory:
|
|
119
127
|
|
|
@@ -15,9 +15,10 @@ Bash tool_use 명령을 phase deny-list 와 대조해 위반을 기계적으로
|
|
|
15
15
|
한계 1: claude-code 세션 jsonl 만 스캔한다. codex/antigravity lead·worker 는 동일 형식의
|
|
16
16
|
tool_use transcript 를 ~/.claude/projects 에 남기지 않으므로 이 스캐너의 사정권 밖이며,
|
|
17
17
|
해당 런타임의 금지 행위는 여전히 수동 감사에 의존한다.
|
|
18
|
-
한계 2: deny-list 는
|
|
19
|
-
등장하는 경우(예: `git commit -m "... npm
|
|
20
|
-
`cd <path> && <cmd>` 형태 때문에 명령
|
|
18
|
+
한계 2: deny-list 는 heredoc 본문을 걷어낸(strip_heredoc_bodies) 명령 문자열 전체를
|
|
19
|
+
검색하므로, 금지 토큰이 인자/메시지에 등장하는 경우(예: `git commit -m "... npm
|
|
20
|
+
publish ..."`)는 여전히 위반으로 잡힐 수 있다. `cd <path> && <cmd>` 형태 때문에 명령
|
|
21
|
+
head 앵커링은 불가하므로 이 잔여 오탐은 감수한다.
|
|
21
22
|
"""
|
|
22
23
|
from __future__ import annotations
|
|
23
24
|
|
|
@@ -47,6 +48,43 @@ _UNIVERSAL = [
|
|
|
47
48
|
]
|
|
48
49
|
_NON_HANDOFF_PUSH = ("git push", re.compile(r"\bgit\s+push\b"))
|
|
49
50
|
|
|
51
|
+
_HEREDOC_START = re.compile(r"<<-?\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\1")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def strip_heredoc_bodies(command: str) -> str:
|
|
55
|
+
"""heredoc 본문을 걷어낸 명령 문자열.
|
|
56
|
+
|
|
57
|
+
heredoc 안의 토큰은 실행되는 명령이 아니라 파일에 **기록되는 데이터**다.
|
|
58
|
+
implementation-planning 은 배포 명령을 서술하는 계획서를 산출물로 내고 그
|
|
59
|
+
문서를 `python3 - <<'PY'` / `cat > … <<EOF` 로 패치하므로, 본문을 그대로
|
|
60
|
+
스캔하면 정상 경로에서 매 run 오탐이 난다. heredoc 시작 줄과 종료 구분자는
|
|
61
|
+
남겨 실제 명령 위치의 토큰은 계속 잡는다.
|
|
62
|
+
|
|
63
|
+
종료 구분자가 뒤에 없으면 아무것도 걷어내지 않는다 — `python3 -c "1 << shift"`
|
|
64
|
+
같은 시프트 연산이 heredoc 으로 오인돼 뒤따르는 진짜 명령을 숨기지 않도록,
|
|
65
|
+
본문 제거는 짝이 맞는 구분자를 실제로 찾았을 때만 한다.
|
|
66
|
+
"""
|
|
67
|
+
lines = command.split("\n")
|
|
68
|
+
kept: list[str] = []
|
|
69
|
+
index = 0
|
|
70
|
+
while index < len(lines):
|
|
71
|
+
line = lines[index]
|
|
72
|
+
kept.append(line)
|
|
73
|
+
index += 1
|
|
74
|
+
start = _HEREDOC_START.search(line)
|
|
75
|
+
if not start:
|
|
76
|
+
continue
|
|
77
|
+
delimiter = start.group(2)
|
|
78
|
+
end = next(
|
|
79
|
+
(i for i in range(index, len(lines)) if lines[i].strip() == delimiter),
|
|
80
|
+
None,
|
|
81
|
+
)
|
|
82
|
+
if end is None:
|
|
83
|
+
continue
|
|
84
|
+
kept.append(lines[end])
|
|
85
|
+
index = end + 1
|
|
86
|
+
return "\n".join(kept)
|
|
87
|
+
|
|
50
88
|
|
|
51
89
|
def forbidden_patterns_for(task_type: str) -> list[tuple[str, re.Pattern]]:
|
|
52
90
|
"""task_type(=phase profile) 의 deny-list. 모든 phase 가 _UNIVERSAL 을 받고,
|
|
@@ -104,8 +142,9 @@ def _scan_one(
|
|
|
104
142
|
if block.get("type") != "tool_use" or block.get("name") != "Bash":
|
|
105
143
|
continue
|
|
106
144
|
command = (block.get("input") or {}).get("command") or ""
|
|
145
|
+
executed = strip_heredoc_bodies(command)
|
|
107
146
|
for label, pattern in patterns:
|
|
108
|
-
if pattern.search(
|
|
147
|
+
if pattern.search(executed):
|
|
109
148
|
hits.append((label, command))
|
|
110
149
|
break # one violation per command — most-specific pattern wins
|
|
111
150
|
return hits
|
|
@@ -12,10 +12,12 @@ from dataclasses import dataclass
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import List, Tuple
|
|
14
14
|
|
|
15
|
-
# scripts/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
# scripts/ (repo) and python/ (installed under ~/.okstra/lib) are not packages;
|
|
16
|
+
# insert whichever exists so okstra_ctl is importable directly.
|
|
17
|
+
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
18
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
19
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
19
21
|
|
|
20
22
|
from okstra_ctl.md_table import split_pipe_row # noqa: E402
|
|
21
23
|
|
|
@@ -24,10 +24,10 @@ import re
|
|
|
24
24
|
import sys
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if str(
|
|
30
|
-
|
|
27
|
+
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
28
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
29
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
31
31
|
|
|
32
32
|
from okstra_ctl.clarification_items import ( # noqa: E402
|
|
33
33
|
parse_clarification_items,
|
|
@@ -11,13 +11,14 @@ import sys
|
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
# Make the
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Make the okstra packages importable however this validator is reached:
|
|
15
|
+
# ``scripts/`` next to the repo checkout, ``python/`` next to the installed
|
|
16
|
+
# copy under ~/.okstra/lib. The manifests advertise the bare script path, so
|
|
17
|
+
# it must self-bootstrap rather than rely on an inherited PYTHONPATH.
|
|
17
18
|
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
18
|
-
|
|
19
|
-
if
|
|
20
|
-
|
|
19
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
20
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
21
22
|
|
|
22
23
|
try:
|
|
23
24
|
from okstra_ctl.final_report_schema import (
|
|
@@ -57,6 +58,9 @@ from okstra_ctl.design_surfaces import ( # noqa: E402
|
|
|
57
58
|
detect_design_surfaces,
|
|
58
59
|
expected_prep_plan_item_id,
|
|
59
60
|
)
|
|
61
|
+
from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
62
|
+
validate_final_verification_prompt_paths,
|
|
63
|
+
)
|
|
60
64
|
|
|
61
65
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
62
66
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
@@ -366,6 +370,48 @@ def effective_run_task_type(run_manifest: dict, task_manifest: dict) -> str:
|
|
|
366
370
|
).strip()
|
|
367
371
|
|
|
368
372
|
|
|
373
|
+
def _validate_final_verification_initial_prompts(
|
|
374
|
+
data: dict,
|
|
375
|
+
failures: list[str],
|
|
376
|
+
) -> None:
|
|
377
|
+
"""Validate the selected initial analysis prompts persisted for the run."""
|
|
378
|
+
if data.get("taskType") != "final-verification":
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
run_manifest = data.get("runManifest") or {}
|
|
382
|
+
team_contract = run_manifest.get("teamContract") or {}
|
|
383
|
+
selected_workers = team_contract.get("requiredWorkerRoles") or []
|
|
384
|
+
selected_ids = {
|
|
385
|
+
str(worker.get("workerId") or "").strip()
|
|
386
|
+
for worker in selected_workers
|
|
387
|
+
if isinstance(worker, dict)
|
|
388
|
+
}
|
|
389
|
+
project_root = Path(data["projectRoot"])
|
|
390
|
+
team_state = data.get("teamState") or {}
|
|
391
|
+
prompt_paths: dict[str, Path] = {}
|
|
392
|
+
for worker in team_state.get("workers") or []:
|
|
393
|
+
if not isinstance(worker, dict):
|
|
394
|
+
continue
|
|
395
|
+
worker_id = str(worker.get("workerId") or "").strip()
|
|
396
|
+
prompt_value = str(worker.get("promptPath") or "").strip()
|
|
397
|
+
if (
|
|
398
|
+
worker_id not in selected_ids
|
|
399
|
+
or worker_id == "report-writer"
|
|
400
|
+
or not prompt_value
|
|
401
|
+
or "-reverify-r" in Path(prompt_value).name
|
|
402
|
+
):
|
|
403
|
+
continue
|
|
404
|
+
prompt_path = Path(prompt_value)
|
|
405
|
+
prompt_paths[worker_id] = (
|
|
406
|
+
prompt_path if prompt_path.is_absolute() else project_root / prompt_path
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
errors = validate_final_verification_prompt_paths(prompt_paths)
|
|
410
|
+
failures.extend(
|
|
411
|
+
f"final-verification prompt contract: {error}" for error in errors
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
369
415
|
def _is_legal_concurrent_run_skip(
|
|
370
416
|
team_create: object, concurrent_run_authorized: bool
|
|
371
417
|
) -> bool:
|
|
@@ -3523,6 +3569,15 @@ def main() -> int:
|
|
|
3523
3569
|
# contain every required section. Substring checks below are a
|
|
3524
3570
|
# safety net for hand-edited or pre-v1.0 reports.
|
|
3525
3571
|
task_type = effective_run_task_type(run_manifest, task_manifest)
|
|
3572
|
+
_validate_final_verification_initial_prompts(
|
|
3573
|
+
{
|
|
3574
|
+
"taskType": task_type,
|
|
3575
|
+
"projectRoot": project_root,
|
|
3576
|
+
"runManifest": run_manifest,
|
|
3577
|
+
"teamState": team_state,
|
|
3578
|
+
},
|
|
3579
|
+
failures,
|
|
3580
|
+
)
|
|
3526
3581
|
report_contracts = _normalize_report_contracts(
|
|
3527
3582
|
run_manifest.get("reportContracts")
|
|
3528
3583
|
)
|
|
@@ -17,10 +17,12 @@ import re
|
|
|
17
17
|
import sys
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
-
# scripts/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
# scripts/ (repo) and python/ (installed under ~/.okstra/lib) are not packages;
|
|
21
|
+
# insert whichever exists so okstra_ctl is importable directly.
|
|
22
|
+
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
23
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
24
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
24
26
|
|
|
25
27
|
from okstra_ctl.md_table import split_pipe_row # noqa: E402
|
|
26
28
|
|
|
@@ -10,9 +10,10 @@ import sys
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
14
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
15
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
16
17
|
|
|
17
18
|
from okstra_ctl.work_categories import WORK_CATEGORIES # noqa: E402
|
|
18
19
|
from okstra_ctl.fanout import topological_order, CycleError # noqa: E402
|
|
@@ -12,10 +12,12 @@ import sys
|
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
# scripts/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
# scripts/ (repo) and python/ (installed under ~/.okstra/lib) are not packages;
|
|
16
|
+
# insert whichever exists so okstra_ctl is importable directly.
|
|
17
|
+
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
18
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
19
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
19
21
|
|
|
20
22
|
from okstra_ctl.improvement_lenses import (
|
|
21
23
|
LENSES,
|
|
@@ -26,6 +26,18 @@ from dataclasses import dataclass, field
|
|
|
26
26
|
from datetime import datetime
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
|
+
# scripts/ (repo) and python/ (installed under ~/.okstra/lib) are not packages;
|
|
30
|
+
# insert whichever exists so okstra_ctl is importable directly.
|
|
31
|
+
_VALIDATORS_DIR = Path(__file__).resolve().parent
|
|
32
|
+
for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
|
|
33
|
+
if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
|
|
34
|
+
sys.path.insert(0, str(_ssot_dir))
|
|
35
|
+
|
|
36
|
+
from okstra_ctl.worker_heartbeat import ( # noqa: E402
|
|
37
|
+
HEARTBEAT_LINE_RE,
|
|
38
|
+
HEARTBEAT_MAX_GAP_SECONDS,
|
|
39
|
+
)
|
|
40
|
+
|
|
29
41
|
_DISPATCHED_STATUSES = {"completed", "timeout", "error", "in-progress"}
|
|
30
42
|
_WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed", "tmux-pane"}
|
|
31
43
|
_ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
@@ -39,13 +51,10 @@ _PROGRESS_LINE_RE = re.compile(
|
|
|
39
51
|
re.MULTILINE,
|
|
40
52
|
)
|
|
41
53
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# 계약상 cadence 는 5분. append 직전 측정한 시각과 실제 쓰기 사이 지연을 흡수하는
|
|
47
|
-
# 고정 grace 60초를 더한다.
|
|
48
|
-
_HEARTBEAT_MAX_GAP_SECONDS = 5 * 60 + 60
|
|
54
|
+
# heartbeat 라인 shape 과 cadence 예산은 okstra_ctl.worker_heartbeat 정본을 쓴다 —
|
|
55
|
+
# `okstra worker-liveness` 가 run 도중 같은 판정을 내리므로 정의가 갈리면 안 된다.
|
|
56
|
+
_HEARTBEAT_LINE_RE = HEARTBEAT_LINE_RE
|
|
57
|
+
_HEARTBEAT_MAX_GAP_SECONDS = HEARTBEAT_MAX_GAP_SECONDS
|
|
49
58
|
|
|
50
59
|
# Phase 5/6 진입 전 lead 가 Read 해야 하는 implementation 프로파일 sidecar.
|
|
51
60
|
# 절대 경로는 레이어(repo / runtime / 설치본)마다 다르지만 basename 은 동일하다.
|
package/src/cli-registry.mjs
CHANGED
|
@@ -159,6 +159,13 @@ export const COMMAND_REGISTRY = [
|
|
|
159
159
|
category: "introspection",
|
|
160
160
|
summary: ["Inventory wrapper sidecar logs by size and task (read-only)"],
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: "worker-liveness",
|
|
164
|
+
module: "./commands/inspect/worker-liveness.mjs",
|
|
165
|
+
export: "run",
|
|
166
|
+
category: "introspection",
|
|
167
|
+
summary: ["Report whether pending workers are still alive (read-only)"],
|
|
168
|
+
},
|
|
162
169
|
{
|
|
163
170
|
name: "error-report",
|
|
164
171
|
module: "./commands/inspect/error-report.mjs",
|
|
@@ -6,9 +6,11 @@ Usage:
|
|
|
6
6
|
okstra log-report [--project-root <dir>] [--cwd <dir>] [--top <N>] [--json]
|
|
7
7
|
|
|
8
8
|
Output: JSON { ok, projectRoot, logsRoot, topLargest[], perTask[], totals }.
|
|
9
|
-
Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Scans <project-root>/.okstra/tasks/**/runs/*/prompts/*.log. Each entry reports
|
|
10
|
+
the wrapper transcript as transcriptBytes and its sibling prompt Markdown as
|
|
11
|
+
promptBytes; sizeBytes remains the transcript-size compatibility field. Sizes
|
|
12
|
+
are raw bytes and mtimes are epoch seconds — the caller formats KB/MB and ages.
|
|
13
|
+
Read-only: it never deletes logs (cleanup commands stay in the skill).
|
|
12
14
|
`;
|
|
13
15
|
|
|
14
16
|
export async function run(args) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra worker-liveness — report whether pending workers are still alive
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra worker-liveness [--audit <path>]... [--prompt <path>]...
|
|
7
|
+
[--max-idle <seconds>] [--launch-grace <seconds>] [--json]
|
|
8
|
+
|
|
9
|
+
--audit a claude-worker audit sidecar; stale past the heartbeat cadence means
|
|
10
|
+
the in-process worker hung.
|
|
11
|
+
--prompt a CLI-wrapper prompt-history path; no sibling .log or .status.json
|
|
12
|
+
past the launch grace means the wrapper never started.
|
|
13
|
+
|
|
14
|
+
Output: JSON { ok, checkedAt, probes[], unhealthy[] }. Exit 1 when any worker is
|
|
15
|
+
stalled or did not launch, so a poll loop can branch on the exit code. Read-only:
|
|
16
|
+
it reports, it never kills or re-dispatches.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
export async function run(args) {
|
|
20
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
21
|
+
process.stdout.write(USAGE);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = await runPythonModule({
|
|
26
|
+
module: "okstra_ctl.worker_liveness",
|
|
27
|
+
args,
|
|
28
|
+
stdio: "inherit-stdout",
|
|
29
|
+
});
|
|
30
|
+
return result.code;
|
|
31
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { checkProject } from "./check-project.mjs";
|
|
2
2
|
import { runEnsureInstalled } from "./install.mjs";
|
|
3
|
+
import { missingHelperScripts } from "../../lib/helper-scripts.mjs";
|
|
4
|
+
import { resolveInstalledScript } from "../../lib/python-helper.mjs";
|
|
5
|
+
import { resolvePaths } from "../../lib/paths.mjs";
|
|
3
6
|
|
|
4
7
|
const USAGE = `okstra preflight — one-call skill preflight (ensure-installed + check-project)
|
|
5
8
|
|
|
@@ -11,8 +14,9 @@ verifies the target project has .okstra/project.json. Prints one JSON object:
|
|
|
11
14
|
|
|
12
15
|
ok: true {ok, projectRoot, projectJsonPath, projectId}
|
|
13
16
|
ok: false {ok, stage, reason, ...}
|
|
14
|
-
stage: install |
|
|
15
|
-
|
|
17
|
+
stage: install | helper_scripts_missing | resolve |
|
|
18
|
+
project_json_missing | project_json_invalid |
|
|
19
|
+
python | parse
|
|
16
20
|
|
|
17
21
|
Options:
|
|
18
22
|
--runtime <name> Runtime whose assets must be present (e.g. claude-code);
|
|
@@ -48,6 +52,12 @@ function emit(payload) {
|
|
|
48
52
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
// A BLOCKING lead step calls these mid-run; resolving them here turns a stale
|
|
56
|
+
// install into an up-front failure instead of a dead end hours into a run.
|
|
57
|
+
async function findMissingHelperScripts() {
|
|
58
|
+
return missingHelperScripts(await resolvePaths(), resolveInstalledScript);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
export async function run(args) {
|
|
52
62
|
if (args.includes("--help") || args.includes("-h")) {
|
|
53
63
|
process.stdout.write(USAGE);
|
|
@@ -76,6 +86,19 @@ export async function run(args) {
|
|
|
76
86
|
return installCode === 2 ? 2 : 1;
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
const missingHelpers = await findMissingHelperScripts();
|
|
90
|
+
if (missingHelpers.length > 0) {
|
|
91
|
+
emit({
|
|
92
|
+
ok: false,
|
|
93
|
+
stage: "helper_scripts_missing",
|
|
94
|
+
missingHelpers,
|
|
95
|
+
reason:
|
|
96
|
+
`${missingHelpers.length} helper script(s) a lead prompt can call are not ` +
|
|
97
|
+
"installed — run 'okstra install' to repair before starting a run.",
|
|
98
|
+
});
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
const { code, payload } = await checkProject({ cwd: opts.cwd });
|
|
80
103
|
emit(payload);
|
|
81
104
|
return code;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Single source of truth for the python helper scripts that `okstra <cmd>`
|
|
2
|
+
// subcommands front through runInstalledScript(). Preflight asserts every one
|
|
3
|
+
// of them resolves under ~/.okstra/bin so a stale install fails at the cheap
|
|
4
|
+
// environment gate instead of at the blocking lead step that calls it mid-run.
|
|
5
|
+
// A contract test keeps this list in step with the `scriptName:` literals in
|
|
6
|
+
// src/commands/**.
|
|
7
|
+
|
|
8
|
+
export const HELPER_SCRIPT_NAMES = Object.freeze([
|
|
9
|
+
"okstra-error-log.py",
|
|
10
|
+
"okstra-incremental-carry.py",
|
|
11
|
+
"okstra-incremental-scope.py",
|
|
12
|
+
"okstra-inject-report-index.py",
|
|
13
|
+
"okstra-render-final-report.py",
|
|
14
|
+
"okstra-render-report-views.py",
|
|
15
|
+
"okstra-spawn-followups.py",
|
|
16
|
+
"okstra-token-usage.py",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// `resolve` is the same resolver the wrappers call, so preflight cannot pass a
|
|
20
|
+
// script the dispatch path would then fail to find.
|
|
21
|
+
export function missingHelperScripts(paths, resolve) {
|
|
22
|
+
return HELPER_SCRIPT_NAMES.filter((name) => !resolve(paths, name));
|
|
23
|
+
}
|
|
@@ -4,7 +4,7 @@ import { join, resolve as resolvePath } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
6
6
|
|
|
7
|
-
function resolveInstalledScript(paths, scriptName) {
|
|
7
|
+
export function resolveInstalledScript(paths, scriptName) {
|
|
8
8
|
// Prefer the installed copy under ~/.okstra/bin (what production users run);
|
|
9
9
|
// fall back to the in-repo source when invoked from a checkout that has not
|
|
10
10
|
// been installed (dev / CI).
|