okstra 0.18.2 → 0.18.3
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/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from .blocks import na_block, usage_block
|
|
7
8
|
from .claude import claude_session_totals, find_claude_team_sessions
|
|
@@ -11,6 +12,76 @@ from .paths import claude_project_dir, utc_now
|
|
|
11
12
|
from .pricing import codex_cost_usd, gemini_cost_usd
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def match_prefixes(worker_id: str) -> list[str]:
|
|
16
|
+
"""Return the agentName prefixes that should be attributed to ``worker_id``.
|
|
17
|
+
|
|
18
|
+
The Agent harness records the `name` arg on every dispatch as `agentName`
|
|
19
|
+
in the subagent jsonl. Lead frequently appends suffixes (`-002`,
|
|
20
|
+
`-reverify-r1`, `-impl`, `-2`) when it dispatches the same role multiple
|
|
21
|
+
times or in different sub-flows. We treat every `agentName` matching one of
|
|
22
|
+
these prefixes — either exactly or as `<prefix>-<suffix>` — as belonging
|
|
23
|
+
to this worker so its tokens get aggregated. For implementation runs the
|
|
24
|
+
executor variant `<provider>-executor` is also attributed back to the
|
|
25
|
+
matching provider worker.
|
|
26
|
+
"""
|
|
27
|
+
if not worker_id:
|
|
28
|
+
return []
|
|
29
|
+
if worker_id == "report-writer":
|
|
30
|
+
return ["report-writer"]
|
|
31
|
+
prefixes = [worker_id]
|
|
32
|
+
if not worker_id.endswith("-worker"):
|
|
33
|
+
prefixes.append(f"{worker_id}-worker")
|
|
34
|
+
prefixes.append(f"{worker_id}-executor")
|
|
35
|
+
return prefixes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def agent_matches(agent_name: str, prefixes: list[str]) -> bool:
|
|
39
|
+
if not agent_name:
|
|
40
|
+
return False
|
|
41
|
+
for prefix in prefixes:
|
|
42
|
+
if agent_name == prefix or agent_name.startswith(f"{prefix}-"):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _aggregate_totals(items: list[dict]) -> dict:
|
|
48
|
+
"""Sum token + tool counters across multiple session totals dicts.
|
|
49
|
+
|
|
50
|
+
`startedAt` / `endedAt` collapse to the union window; `durationMs` is
|
|
51
|
+
recomputed from that window so re-tries and convergence rounds count
|
|
52
|
+
against a single contiguous span. `model` and `agentName` keep the first
|
|
53
|
+
non-empty value (the canonical role identity).
|
|
54
|
+
"""
|
|
55
|
+
aggregate: dict = {
|
|
56
|
+
"totalTokens": 0, "inputTokens": 0, "outputTokens": 0,
|
|
57
|
+
"cacheCreationTokens": 0, "cacheReadTokens": 0,
|
|
58
|
+
"toolUses": 0, "durationMs": 0,
|
|
59
|
+
"agentName": None, "model": None,
|
|
60
|
+
"startedAt": None, "endedAt": None,
|
|
61
|
+
}
|
|
62
|
+
for t in items:
|
|
63
|
+
for k in ("totalTokens", "inputTokens", "outputTokens",
|
|
64
|
+
"cacheCreationTokens", "cacheReadTokens", "toolUses"):
|
|
65
|
+
aggregate[k] += t.get(k, 0) or 0
|
|
66
|
+
if aggregate["agentName"] is None and t.get("agentName"):
|
|
67
|
+
aggregate["agentName"] = t["agentName"]
|
|
68
|
+
if aggregate["model"] is None and t.get("model"):
|
|
69
|
+
aggregate["model"] = t["model"]
|
|
70
|
+
s, e = t.get("startedAt"), t.get("endedAt")
|
|
71
|
+
if s and (aggregate["startedAt"] is None or s < aggregate["startedAt"]):
|
|
72
|
+
aggregate["startedAt"] = s
|
|
73
|
+
if e and (aggregate["endedAt"] is None or e > aggregate["endedAt"]):
|
|
74
|
+
aggregate["endedAt"] = e
|
|
75
|
+
if aggregate["startedAt"] and aggregate["endedAt"]:
|
|
76
|
+
try:
|
|
77
|
+
a = datetime.fromisoformat(aggregate["startedAt"].replace("Z", "+00:00"))
|
|
78
|
+
b = datetime.fromisoformat(aggregate["endedAt"].replace("Z", "+00:00"))
|
|
79
|
+
aggregate["durationMs"] = max(0, int((b - a).total_seconds() * 1000))
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
return aggregate
|
|
83
|
+
|
|
84
|
+
|
|
14
85
|
def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
15
86
|
state = json.loads(team_state_path.read_text())
|
|
16
87
|
cwd = project_root or _infer_project_root(team_state_path, state)
|
|
@@ -26,19 +97,20 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
26
97
|
team_name = f"okstra-{task_id}" if task_id else ""
|
|
27
98
|
lead_sid = (state.get("lead") or {}).get("sessionId")
|
|
28
99
|
|
|
29
|
-
# 1) Claude sessions (lead + claude-side workers).
|
|
100
|
+
# 1) Claude sessions (lead + claude-side workers). Cache totals at scan
|
|
101
|
+
# time so we don't re-read the jsonl when a worker matches multiple
|
|
102
|
+
# sessions.
|
|
30
103
|
claude_sessions = find_claude_team_sessions(cwd, team_name, lead_sid)
|
|
31
|
-
by_agent: dict[str, tuple[str, Path]] = {}
|
|
104
|
+
by_agent: dict[str, list[tuple[str, Path, dict]]] = {}
|
|
32
105
|
lead_path: Path | None = None
|
|
33
106
|
for sid, path in claude_sessions.items():
|
|
34
107
|
if sid == lead_sid:
|
|
35
108
|
lead_path = path
|
|
36
109
|
continue
|
|
37
|
-
# Read agentName lazily.
|
|
38
110
|
totals = claude_session_totals(path)
|
|
39
111
|
agent = totals.get("agentName")
|
|
40
112
|
if agent:
|
|
41
|
-
by_agent[
|
|
113
|
+
by_agent.setdefault(agent, []).append((sid, path, totals))
|
|
42
114
|
|
|
43
115
|
# Lead.
|
|
44
116
|
if lead_path is not None:
|
|
@@ -50,35 +122,39 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
50
122
|
f"lead session jsonl not found under {claude_project_dir(cwd)} (sessionId={lead_sid})"
|
|
51
123
|
)
|
|
52
124
|
|
|
53
|
-
# Workers
|
|
125
|
+
# Workers — match by prefix and aggregate every session that belongs to
|
|
126
|
+
# the same role (re-dispatches with `-002`, convergence `-reverify-r1`,
|
|
127
|
+
# implementation `-executor`, report-writer `-impl` / `-2`, etc.).
|
|
54
128
|
for worker in state.get("workers", []):
|
|
55
129
|
worker_id = worker.get("workerId")
|
|
56
130
|
agent = worker.get("agent")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
wrapper = by_agent[cand]
|
|
69
|
-
break
|
|
70
|
-
if wrapper is None:
|
|
71
|
-
worker["usage"] = na_block(f"no Claude subagent jsonl found with agentName matching {agent_name_candidates}")
|
|
131
|
+
prefixes = match_prefixes(worker_id) if worker_id else []
|
|
132
|
+
|
|
133
|
+
matched: list[tuple[str, Path, dict]] = []
|
|
134
|
+
for agent_name, entries in by_agent.items():
|
|
135
|
+
if agent_matches(agent_name, prefixes):
|
|
136
|
+
matched.extend(entries)
|
|
137
|
+
|
|
138
|
+
if not matched:
|
|
139
|
+
worker["usage"] = na_block(
|
|
140
|
+
f"no Claude subagent jsonl found with agentName matching prefixes {prefixes}"
|
|
141
|
+
)
|
|
72
142
|
continue
|
|
73
|
-
sid, path = wrapper
|
|
74
|
-
totals = claude_session_totals(path)
|
|
75
|
-
block = usage_block(totals, source="claude-jsonl")
|
|
76
|
-
block["sessionId"] = sid
|
|
77
143
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
# Stable order by startedAt so the "primary" session is the first one.
|
|
145
|
+
matched.sort(key=lambda x: x[2].get("startedAt") or "")
|
|
146
|
+
primary_sid, _primary_path, _primary_totals = matched[0]
|
|
147
|
+
aggregate = _aggregate_totals([t for _, _, t in matched])
|
|
148
|
+
block = usage_block(aggregate, source="claude-jsonl")
|
|
149
|
+
block["sessionId"] = primary_sid
|
|
150
|
+
if len(matched) > 1:
|
|
151
|
+
block["additionalSessionIds"] = [sid for sid, _, _ in matched[1:]]
|
|
152
|
+
block["matchedAgentNames"] = sorted({t.get("agentName") for _, _, t in matched if t.get("agentName")})
|
|
153
|
+
|
|
154
|
+
# For codex/gemini workers, find every CLI session that fell inside the
|
|
155
|
+
# aggregated wrapper window.
|
|
156
|
+
wrapper_start = aggregate.get("startedAt") or ""
|
|
157
|
+
wrapper_end = aggregate.get("endedAt") or ""
|
|
82
158
|
if agent in ("codex", "gemini"):
|
|
83
159
|
if agent == "codex":
|
|
84
160
|
cli = find_codex_session(cwd, wrapper_start, wrapper_end)
|
|
@@ -90,7 +90,7 @@ Behaviour contract:
|
|
|
90
90
|
After the spawner completes, the report-writer worker MUST update Section 6 ("Recommended Next Steps") to list every newly created task-key together with its entry command, so the user can pick the follow-up up immediately:
|
|
91
91
|
|
|
92
92
|
```
|
|
93
|
-
- Follow-up: `<task-group>/<new-task-id>` — `okstra --task-key <task-group>/<new-task-id> --task-type <suggested>`
|
|
93
|
+
- Follow-up: `<task-group>/<new-task-id>` — Claude Code 세션 안 `/okstra-run task-key=<task-group>/<new-task-id> task-type=<suggested>` / 별도 터미널 `scripts/okstra.sh --task-key <task-group>/<new-task-id> --task-type <suggested>`
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
## Phase 7 token-usage collector (BLOCKING)
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
> 다음 `implementation` run은 아래 체크박스가 `[x]`로 표시되어 있을 때에만 진입할 수 있습니다 (`okstra_ctl.run._validate_approved_plan` 가 이 마커를 line-anchored 정규식으로 검사하여 통과/거부합니다). 본문(`Sections 1`–`4.5`)을 끝까지 읽고, `4.5.9 Open Questions`가 비어 있거나 모두 해소된 뒤 승인해 주세요.
|
|
14
14
|
|
|
15
15
|
- 승인 여부 (사용자가 직접 편집): `- [ ] Approved` ← 승인하려면 `[ ]` 를 `[x]` 로 변경하여 저장하세요.
|
|
16
|
-
- 승인 후 다음 단계 명령어 (방법 A — 수동 편집):
|
|
17
|
-
-
|
|
16
|
+
- 승인 후 다음 단계 명령어 (방법 A — 수동 편집):
|
|
17
|
+
- Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type=implementation approved-plan=<이 보고서 경로>`
|
|
18
|
+
- 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type implementation --approved-plan <이 보고서 경로>`
|
|
19
|
+
- 승인 + 실행 한 번에 (방법 B — 진입 명령 자체를 승인 의사로):
|
|
20
|
+
- Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type=implementation approved-plan=<이 보고서 경로> approve`
|
|
21
|
+
- 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type implementation --approved-plan <이 보고서 경로> --approve`
|
|
18
22
|
- 방법 B 는 `--approve` 입력 행위 자체를 승인 의사로 모델링합니다. 런타임이 본 블록의 체크박스를 자동으로 `[x]` 로 바꾸고, 본 섹션 하단에 `승인 일시 (CLI ack): <ISO8601>` audit 라인을 한 줄 덧붙입니다.
|
|
19
23
|
- 승인을 보류하거나 거부하려면 체크박스는 `[ ]` 로 두고 `--approve` 도 사용하지 마세요. 필요한 변경 사항은 `4.5.9 Open Questions` 또는 `Section 5 Clarification Requests` 에 기록한 뒤 같은 phase 를 재실행해 주세요.
|
|
20
24
|
|
|
@@ -270,7 +274,11 @@ H1 이 `skip` 이거나 H3 가 `cancel` 인 경우, 본 섹션 다음의 4.6.4 ~
|
|
|
270
274
|
- `resolved`: 다음 run에서 lead가 답변을 받아 검증을 마쳤습니다.
|
|
271
275
|
- `obsolete`: 이후 분석 결과로 더 이상 필요 없어진 항목입니다.
|
|
272
276
|
|
|
273
|
-
이 보고서에 답을 채우신 뒤에는
|
|
277
|
+
이 보고서에 답을 채우신 뒤에는 한 줄로 같은 phase를 다시 실행하실 수 있습니다(자동으로 `$EDITOR`가 이 파일을 열고, 저장하면 같은 phase가 `--clarification-response`로 carry-in 되어 재실행됩니다).
|
|
278
|
+
- Claude Code 세션 안: `/okstra-run resume-clarification task-key={{TASK_KEY}}`
|
|
279
|
+
- 별도 터미널: `scripts/okstra.sh --resume-clarification --task-key {{TASK_KEY}}`
|
|
280
|
+
|
|
281
|
+
스크립트로 자동화하실 때는 셸 형식 `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}} --clarification-response <이 파일 경로>`도 그대로 사용하실 수 있습니다. Node `okstra` admin CLI 는 `--task-key`/`--task-type`/`--resume-clarification` 을 받지 않으므로 위 두 진입점 중 하나를 사용하세요.
|
|
274
282
|
|
|
275
283
|
### 5.1 추가 자료 요청 (Additional Materials Requested)
|
|
276
284
|
|
|
@@ -298,16 +306,22 @@ H1 이 `skip` 이거나 H3 가 `cancel` 인 경우, 본 섹션 다음의 4.6.4 ~
|
|
|
298
306
|
|
|
299
307
|
This section is **always present** in every final report — never omit the heading. If there are no concrete actions to take, write the single line `- No further action required. Final verdict in section 2 stands.` under the heading and stop.
|
|
300
308
|
|
|
301
|
-
When concrete actions exist, list them as a numbered list using the rules below. Each item must include the exact command(s) the user can copy-paste.
|
|
309
|
+
When concrete actions exist, list them as a numbered list using the rules below. Each item must include the exact command(s) the user can copy-paste. Show **both** the Claude Code in-session form (`/okstra-run …`) and the external-terminal shell form (`scripts/okstra.sh …`) — the Node `okstra` admin CLI does NOT accept `--task-key` / `--task-type` / `--resume-clarification`. Prefer the `task-key` shorthand for follow-up runs and `resume-clarification` for clarification answer turn-arounds; show the equivalent full-args form only when useful.
|
|
302
310
|
|
|
303
311
|
1. **Highest-priority next action.** State what to do and why in one sentence, then the command. Example shortcut forms:
|
|
304
|
-
- Same phase rerun:
|
|
305
|
-
|
|
312
|
+
- Same phase rerun:
|
|
313
|
+
- Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type={{TASK_TYPE}}`
|
|
314
|
+
- 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}}`
|
|
315
|
+
- Next phase (omit `task-type` to use the manifest's `workflow.nextRecommendedPhase` automatically when it is a concrete phase, not `pending-routing-decision` / `done-or-follow-up`):
|
|
316
|
+
- Claude Code 세션 안: `/okstra-run task-key={{TASK_KEY}} task-type=<next-phase>`
|
|
317
|
+
- 별도 터미널: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type <next-phase>`
|
|
306
318
|
2. **Additional verification needed before implementation or release.** List read-only checks (test commands, log queries, dashboard URLs) that the user should run before moving to the next phase. No state-mutating commands here.
|
|
307
319
|
3. **Follow-up tasks or related tasks if needed.** Reference them by `task-key` when they already exist; otherwise describe the new brief to draft.
|
|
308
320
|
4. **If section 5 has any `open` rows**, the highest-priority next step MUST be the clarification turn-around. Show both forms:
|
|
309
|
-
- Preferred (interactive)
|
|
310
|
-
|
|
321
|
+
- Preferred (interactive) — opens this file in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in:
|
|
322
|
+
- Claude Code 세션 안: `/okstra-run resume-clarification task-key={{TASK_KEY}}`
|
|
323
|
+
- 별도 터미널: `scripts/okstra.sh --resume-clarification --task-key {{TASK_KEY}}`
|
|
324
|
+
- Scripted: `scripts/okstra.sh --task-key {{TASK_KEY}} --task-type {{TASK_TYPE}} --clarification-response <path-to-this-file-after-editing>`.
|
|
311
325
|
|
|
312
326
|
Empty-state placeholder, copy verbatim when nothing else applies:
|
|
313
327
|
|