okstra 0.43.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +2 -0
- package/runtime/bin/okstra-spawn-followups.py +325 -0
- package/runtime/python/okstra_token_usage/collect.py +11 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +1 -1
- package/src/install.mjs +1 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
package/runtime/agents/SKILL.md
CHANGED
|
@@ -216,6 +216,8 @@ Use agent and subagent names that map cleanly to the selected worker roles. Do n
|
|
|
216
216
|
|
|
217
217
|
Spawn **analysis workers only** in the same turn (Phase 4 in Teams mode; Phase 5 with `run_in_background: true` and no `team_name` when Teams unavailable). Preserve exact roster, role labels, assigned models from the task bundle.
|
|
218
218
|
|
|
219
|
+
**Agent `name` on dispatch (BLOCKING — token-usage attribution depends on it).** Every analysis-worker `Agent(...)` call MUST set `name: "<workerId>-worker"` — `name: "claude-worker"` / `name: "codex-worker"` / `name: "gemini-worker"` — exactly as the report-writer dispatch sets `name: "report-writer"` ([okstra-report-writer](./skills/okstra-report-writer/SKILL.md)). The Agent harness records this `name` as `agentName` in the subagent session jsonl, and the Phase 7 token collector matches each worker's session by that `agentName` (`okstra_token_usage/collect.py`). A worker dispatched **without** `name` produces a session with no `agentName`; the collector cannot attribute it and records the worker as `source: "unavailable"` even though the session exists and is team-tagged (observed in `dev-9692` error-analysis: `claude`/`codex` workers dispatched without `name` → both `unavailable`, while the named `report-writer` collected normally). Convergence reverify dispatches keep the prefix (`<workerId>-worker-reverify-r<N>`); implementation executor/verifier variants keep `<workerId>-worker` / `<workerId>-executor`.
|
|
220
|
+
|
|
219
221
|
The no-`team_name` fallback (Phase 5) is only legal when team-state's `teamCreate.status` is `"error"` for this run. If `teamCreate` is missing or `attempted: false`, the correct action when an Agent dispatch is rejected for a missing team is to GO BACK to Phase 3 and call `TeamCreate` — never to strip `team_name` and continue.
|
|
220
222
|
|
|
221
223
|
### Errors log path wiring (BLOCKING)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OKSTRA follow-up task spawner.
|
|
3
|
+
|
|
4
|
+
Reads the ``followUpTasks[]`` array from a final-report ``data.json``
|
|
5
|
+
(the JSON SSOT for the final-report markdown) and creates stub task
|
|
6
|
+
directories for rows whose ``autoSpawn`` is ``yes`` AND whose ``origin``
|
|
7
|
+
is not the same-task-key ``phase-continuation`` marker.
|
|
8
|
+
|
|
9
|
+
Idempotent: rows whose target directory already exists are reported as
|
|
10
|
+
``existing`` and skipped. Existing directories are NEVER mutated.
|
|
11
|
+
|
|
12
|
+
Output: writes new directories under
|
|
13
|
+
``<project_root>/.okstra/tasks/<task-group>/<new-task-id>/`` with:
|
|
14
|
+
- ``task-manifest.json`` — minimal manifest (schemaVersion 1.0,
|
|
15
|
+
currentStatus ``todo``, workflow.currentPhase = suggestedTaskType,
|
|
16
|
+
workflow.currentPhaseState ``not-started``, parentTaskKey /
|
|
17
|
+
spawnedFromReport recorded under ``relatedTasks``).
|
|
18
|
+
- ``instruction-set/task-brief.md`` — stub brief naming the parent and
|
|
19
|
+
copying the Reason / Scope cells from the data.json row.
|
|
20
|
+
- ``task-index.md`` — short human-readable summary.
|
|
21
|
+
|
|
22
|
+
The script DOES NOT call the okstra runtime; it produces just enough
|
|
23
|
+
on-disk state for the next user-driven entry — ``/okstra-run
|
|
24
|
+
task-key=<new-key> task-type=<suggested>`` inside a Claude Code session,
|
|
25
|
+
or ``scripts/okstra.sh --task-key <new-key> --task-type <suggested>``
|
|
26
|
+
in a separate terminal — to pick the follow-up up and re-render a fully
|
|
27
|
+
canonical manifest on first execution.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
python3 scripts/okstra-spawn-followups.py \\
|
|
31
|
+
<final-report-data.json> \\
|
|
32
|
+
--project-root <abs-path> \\
|
|
33
|
+
--task-group <group-slug> \\
|
|
34
|
+
--parent-task-key <parent-task-key> \\
|
|
35
|
+
[--dry-run]
|
|
36
|
+
|
|
37
|
+
Exit codes:
|
|
38
|
+
0 — at least one follow-up evaluated (including ``skipped`` /
|
|
39
|
+
``existing`` only)
|
|
40
|
+
1 — invocation / parsing failure, or any row failed validation
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import datetime as dt
|
|
46
|
+
import json
|
|
47
|
+
import re
|
|
48
|
+
import sys
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
|
|
51
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
52
|
+
|
|
53
|
+
from okstra_project.dirs import tasks_root # noqa: E402
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
SLUG_RE = re.compile(r"[^a-zA-Z0-9-]+")
|
|
57
|
+
|
|
58
|
+
ALLOWED_TASK_TYPES = {
|
|
59
|
+
"requirements-discovery",
|
|
60
|
+
"error-analysis",
|
|
61
|
+
"implementation-planning",
|
|
62
|
+
"implementation",
|
|
63
|
+
"final-verification",
|
|
64
|
+
"release-handoff",
|
|
65
|
+
}
|
|
66
|
+
ALLOWED_ORIGINS = {
|
|
67
|
+
"phase-continuation",
|
|
68
|
+
"out-of-plan",
|
|
69
|
+
"verifier-concern",
|
|
70
|
+
"scope-boundary",
|
|
71
|
+
"open-question",
|
|
72
|
+
"manual",
|
|
73
|
+
}
|
|
74
|
+
# Origins that point at the SAME task-key (next phase) and therefore
|
|
75
|
+
# must never spawn a new task directory — the user advances via
|
|
76
|
+
# /okstra-run.
|
|
77
|
+
NON_SPAWNING_ORIGINS = {"phase-continuation"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _slugify(value: str) -> str:
|
|
81
|
+
value = value.strip()
|
|
82
|
+
value = SLUG_RE.sub("-", value)
|
|
83
|
+
value = re.sub(r"-+", "-", value)
|
|
84
|
+
return value.strip("-").lower()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_row(row: dict) -> tuple[bool, str]:
|
|
88
|
+
origin = (row.get("origin") or "").strip()
|
|
89
|
+
if origin not in ALLOWED_ORIGINS:
|
|
90
|
+
return False, f"invalid origin: {origin!r}"
|
|
91
|
+
task_type = (row.get("suggestedTaskType") or "").strip()
|
|
92
|
+
if task_type not in ALLOWED_TASK_TYPES:
|
|
93
|
+
return False, f"invalid suggestedTaskType: {task_type!r}"
|
|
94
|
+
if not (row.get("title") or "").strip():
|
|
95
|
+
return False, "title is empty"
|
|
96
|
+
if not (row.get("reason") or "").strip():
|
|
97
|
+
return False, "reason is empty"
|
|
98
|
+
if not (row.get("newTaskId") or "").strip():
|
|
99
|
+
return False, "newTaskId is empty"
|
|
100
|
+
return True, ""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _write_manifest(path: Path, payload: dict) -> None:
|
|
104
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
path.write_text(
|
|
106
|
+
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _data_to_report_path(data_path: Path) -> Path:
|
|
112
|
+
"""Derive the markdown sibling path used in spawned manifests for
|
|
113
|
+
the `parentReportPath` field. Falls back to the data.json itself if
|
|
114
|
+
the suffix is not recognised.
|
|
115
|
+
"""
|
|
116
|
+
name = data_path.name
|
|
117
|
+
if name.endswith(".data.json"):
|
|
118
|
+
return data_path.with_name(name[: -len(".data.json")] + ".md")
|
|
119
|
+
return data_path.with_suffix(".md")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _spawn_one(
|
|
123
|
+
*,
|
|
124
|
+
project_root: Path,
|
|
125
|
+
task_group: str,
|
|
126
|
+
parent_task_key: str,
|
|
127
|
+
parent_report_relative: str,
|
|
128
|
+
row: dict,
|
|
129
|
+
dry_run: bool,
|
|
130
|
+
) -> tuple[str, str]:
|
|
131
|
+
"""Returns (status, target_relative_path).
|
|
132
|
+
|
|
133
|
+
status ∈ {created, existing, skipped, invalid}
|
|
134
|
+
"""
|
|
135
|
+
ok, why = _validate_row(row)
|
|
136
|
+
if not ok:
|
|
137
|
+
return ("invalid", why)
|
|
138
|
+
|
|
139
|
+
new_task_id = _slugify(row["newTaskId"])
|
|
140
|
+
if not new_task_id:
|
|
141
|
+
return ("invalid", "newTaskId slug is empty after normalisation")
|
|
142
|
+
|
|
143
|
+
task_root = (
|
|
144
|
+
tasks_root(project_root)
|
|
145
|
+
/ _slugify(task_group)
|
|
146
|
+
/ new_task_id
|
|
147
|
+
)
|
|
148
|
+
rel = task_root.relative_to(project_root).as_posix()
|
|
149
|
+
if task_root.exists():
|
|
150
|
+
return ("existing", rel)
|
|
151
|
+
if dry_run:
|
|
152
|
+
return ("created", rel)
|
|
153
|
+
|
|
154
|
+
suggested = row["suggestedTaskType"].strip()
|
|
155
|
+
title = row["title"].strip()
|
|
156
|
+
scope = (row.get("scope") or "").strip()
|
|
157
|
+
reason = row["reason"].strip()
|
|
158
|
+
origin = row["origin"].strip()
|
|
159
|
+
priority = (row.get("priority") or "P1").strip()
|
|
160
|
+
ticket_id = (row.get("ticketId") or "").strip()
|
|
161
|
+
new_task_key = f"{task_group}/{new_task_id}"
|
|
162
|
+
now = dt.datetime.now(dt.timezone.utc).isoformat()
|
|
163
|
+
|
|
164
|
+
spawned_meta: dict = {
|
|
165
|
+
"parentTaskKey": parent_task_key,
|
|
166
|
+
"parentReportPath": parent_report_relative,
|
|
167
|
+
"origin": origin,
|
|
168
|
+
"rowId": row.get("id", ""),
|
|
169
|
+
"priority": priority,
|
|
170
|
+
"spawnedAt": now,
|
|
171
|
+
}
|
|
172
|
+
if ticket_id:
|
|
173
|
+
spawned_meta["ticketId"] = ticket_id
|
|
174
|
+
|
|
175
|
+
manifest_payload = {
|
|
176
|
+
"schemaVersion": "1.0",
|
|
177
|
+
"taskGroup": task_group,
|
|
178
|
+
"taskId": new_task_id,
|
|
179
|
+
"taskKey": new_task_key,
|
|
180
|
+
"taskGroupPathSegment": _slugify(task_group),
|
|
181
|
+
"taskIdPathSegment": new_task_id,
|
|
182
|
+
"taskType": suggested,
|
|
183
|
+
"workCategory": "unknown",
|
|
184
|
+
"currentStatus": "todo",
|
|
185
|
+
"spawnedFromFollowUp": spawned_meta,
|
|
186
|
+
"relatedTasks": [
|
|
187
|
+
{"taskKey": parent_task_key, "relation": "parent-followup-source"},
|
|
188
|
+
],
|
|
189
|
+
"workflow": {
|
|
190
|
+
"currentPhase": suggested,
|
|
191
|
+
"currentPhaseState": "not-started",
|
|
192
|
+
"nextRecommendedPhase": suggested,
|
|
193
|
+
"phaseStates": {},
|
|
194
|
+
"awaitingApproval": False,
|
|
195
|
+
"routingStatus": "follow-up-spawned",
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
_write_manifest(task_root / "task-manifest.json", manifest_payload)
|
|
199
|
+
|
|
200
|
+
brief_path = task_root / "instruction-set" / "task-brief.md"
|
|
201
|
+
brief_path.parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
ticket_line = f"- Ticket ID: `{ticket_id}`\n" if ticket_id else ""
|
|
203
|
+
brief_body = (
|
|
204
|
+
f"# Follow-up Task Brief — {new_task_key}\n\n"
|
|
205
|
+
f"- Spawned from: `{parent_task_key}`\n"
|
|
206
|
+
f"{ticket_line}"
|
|
207
|
+
f"- Origin: `{origin}`\n"
|
|
208
|
+
f"- Source report row: `{row.get('id', '')}` in `{parent_report_relative}`\n"
|
|
209
|
+
f"- Suggested task-type: `{suggested}`\n"
|
|
210
|
+
f"- Priority: `{priority}`\n"
|
|
211
|
+
f"- Spawned at: `{now}`\n\n"
|
|
212
|
+
f"## Title\n\n{title}\n\n"
|
|
213
|
+
f"## Scope (files / areas)\n\n{scope or '_(미지정)_'}\n\n"
|
|
214
|
+
f"## Reason / Why deferred from parent run\n\n{reason}\n\n"
|
|
215
|
+
f"## Next step\n\n"
|
|
216
|
+
f"이 stub은 사용자가 정식 진입할 때 자동 갱신됩니다. 다음 명령 중 하나로 시작하세요:\n\n"
|
|
217
|
+
f"- Claude Code 세션 안: `/okstra-run task-key={new_task_key} task-type={suggested}`\n"
|
|
218
|
+
f"- 별도 터미널: `scripts/okstra.sh --task-key {new_task_key} --task-type {suggested}`\n"
|
|
219
|
+
)
|
|
220
|
+
brief_path.write_text(brief_body, encoding="utf-8")
|
|
221
|
+
|
|
222
|
+
index_path = task_root / "task-index.md"
|
|
223
|
+
index_body = (
|
|
224
|
+
f"# {new_task_key} — Follow-up Task (todo)\n\n"
|
|
225
|
+
f"- Parent: `{parent_task_key}`\n"
|
|
226
|
+
f"{ticket_line}"
|
|
227
|
+
f"- Suggested task-type: `{suggested}`\n"
|
|
228
|
+
f"- Origin: `{origin}`\n"
|
|
229
|
+
f"- Priority: `{priority}`\n"
|
|
230
|
+
f"- Spawned from report: `{parent_report_relative}`\n"
|
|
231
|
+
f"- Stub brief: `instruction-set/task-brief.md`\n"
|
|
232
|
+
f"- Status: `todo`\n\n"
|
|
233
|
+
f"이 task는 자동 생성된 follow-up stub입니다. 정식 진입 시 manifest가 재렌더링됩니다.\n"
|
|
234
|
+
)
|
|
235
|
+
index_path.write_text(index_body, encoding="utf-8")
|
|
236
|
+
|
|
237
|
+
return ("created", rel)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main(argv: list[str]) -> int:
|
|
241
|
+
parser = argparse.ArgumentParser(
|
|
242
|
+
description="Spawn follow-up task stubs from a final-report data.json.",
|
|
243
|
+
)
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"data_file",
|
|
246
|
+
type=Path,
|
|
247
|
+
help="Path to the final-report data.json (the JSON SSOT).",
|
|
248
|
+
)
|
|
249
|
+
parser.add_argument("--project-root", type=Path, required=True)
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--task-group",
|
|
252
|
+
required=True,
|
|
253
|
+
help="Task-group slug of the parent task.",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument("--parent-task-key", required=True)
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"--dry-run",
|
|
258
|
+
action="store_true",
|
|
259
|
+
help="Parse and validate only; do not write files.",
|
|
260
|
+
)
|
|
261
|
+
args = parser.parse_args(argv)
|
|
262
|
+
|
|
263
|
+
if not args.data_file.exists():
|
|
264
|
+
print(f"data.json not found: {args.data_file}", file=sys.stderr)
|
|
265
|
+
return 1
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
data = json.loads(args.data_file.read_text(encoding="utf-8"))
|
|
269
|
+
except json.JSONDecodeError as exc:
|
|
270
|
+
print(f"invalid JSON in {args.data_file}: {exc}", file=sys.stderr)
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
rows = data.get("followUpTasks") or []
|
|
274
|
+
if not rows:
|
|
275
|
+
print("followUpTasks is empty — nothing to do.")
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
# Manifests record the markdown sibling rather than data.json so the
|
|
279
|
+
# user-facing report (and not the SSOT) is the cite-able artifact.
|
|
280
|
+
parent_report = _data_to_report_path(args.data_file)
|
|
281
|
+
try:
|
|
282
|
+
parent_report_relative = (
|
|
283
|
+
parent_report.resolve().relative_to(args.project_root.resolve()).as_posix()
|
|
284
|
+
)
|
|
285
|
+
except ValueError:
|
|
286
|
+
parent_report_relative = str(parent_report)
|
|
287
|
+
|
|
288
|
+
results = []
|
|
289
|
+
for row in rows:
|
|
290
|
+
origin = (row.get("origin") or "").strip().lower()
|
|
291
|
+
if origin in NON_SPAWNING_ORIGINS:
|
|
292
|
+
results.append((
|
|
293
|
+
"skipped",
|
|
294
|
+
row.get("newTaskId", ""),
|
|
295
|
+
f"{origin} (advance via /okstra-run, no new task dir)",
|
|
296
|
+
))
|
|
297
|
+
continue
|
|
298
|
+
if (row.get("autoSpawn") or "").strip().lower() != "yes":
|
|
299
|
+
results.append((
|
|
300
|
+
"skipped",
|
|
301
|
+
row.get("newTaskId", ""),
|
|
302
|
+
"autoSpawn != yes",
|
|
303
|
+
))
|
|
304
|
+
continue
|
|
305
|
+
status, info = _spawn_one(
|
|
306
|
+
project_root=args.project_root,
|
|
307
|
+
task_group=args.task_group,
|
|
308
|
+
parent_task_key=args.parent_task_key,
|
|
309
|
+
parent_report_relative=parent_report_relative,
|
|
310
|
+
row=row,
|
|
311
|
+
dry_run=args.dry_run,
|
|
312
|
+
)
|
|
313
|
+
results.append((status, row.get("newTaskId", ""), info))
|
|
314
|
+
|
|
315
|
+
print(f"Follow-up spawn summary ({'dry-run' if args.dry_run else 'live'}):")
|
|
316
|
+
for status, task_id, info in results:
|
|
317
|
+
print(f" - [{status}] {task_id}: {info}")
|
|
318
|
+
|
|
319
|
+
if any(status == "invalid" for status, *_ in results):
|
|
320
|
+
return 1
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -118,6 +118,14 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
118
118
|
claude_sessions = find_claude_team_sessions(cwd, team_name, lead_sid)
|
|
119
119
|
by_agent: dict[str, list[tuple[str, Path, dict]]] = {}
|
|
120
120
|
lead_path: Path | None = None
|
|
121
|
+
# Team-tagged non-lead sessions that carry no agentName. These are almost
|
|
122
|
+
# always a worker dispatched without the Agent `name` arg (so the harness
|
|
123
|
+
# recorded no agentName) — the session exists and is team-tagged, but there
|
|
124
|
+
# is nothing to match it to a workerId by. Surfacing them in usageSummary
|
|
125
|
+
# gives the "unavailable" worker a visible cause instead of vanishing
|
|
126
|
+
# silently (observed in dev-9692 error-analysis: claude/codex workers
|
|
127
|
+
# dispatched without `name` → both unavailable, report-writer named → fine).
|
|
128
|
+
unattributed_sessions: list[str] = []
|
|
121
129
|
for sid, path in claude_sessions.items():
|
|
122
130
|
if sid == lead_sid:
|
|
123
131
|
lead_path = path
|
|
@@ -126,6 +134,8 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
126
134
|
agent = totals.get("agentName")
|
|
127
135
|
if agent:
|
|
128
136
|
by_agent.setdefault(agent, []).append((sid, path, totals))
|
|
137
|
+
else:
|
|
138
|
+
unattributed_sessions.append(sid)
|
|
129
139
|
|
|
130
140
|
# Lead.
|
|
131
141
|
if lead_path is not None:
|
|
@@ -242,6 +252,7 @@ def collect(team_state_path: Path, project_root: Path | None = None) -> dict:
|
|
|
242
252
|
"teamName": team_name,
|
|
243
253
|
"sessionsFound": len(claude_sessions),
|
|
244
254
|
"unmatchedModels": sorted(set(unmatched_models)),
|
|
255
|
+
"unattributedTeamSessions": unattributed_sessions,
|
|
245
256
|
"definitions": {
|
|
246
257
|
"totalTokens": "Sum of input + output + cache_creation + cache_read tokens (raw processed volume; matches Anthropic API breakdown). Cache reads are 95%+ in long sessions.",
|
|
247
258
|
"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.",
|
|
@@ -366,7 +366,7 @@ okstra token-usage /abs/path/to/run/state/team-state-<task-type>-<seq>.json --wr
|
|
|
366
366
|
`okstra token-usage` is a thin Node-side wrapper around the python helper installed at `~/.okstra/bin/okstra-token-usage.py`. Calling the python script directly with `python3 "$HOME/..."` is forbidden — the `$HOME` expansion breaks the literal-token permission match and forces a confirmation prompt every call.
|
|
367
367
|
|
|
368
368
|
The script reads:
|
|
369
|
-
- `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` for the lead and every Claude-side worker (Claude worker, Report writer worker, plus the Claude wrappers around Codex/Gemini workers). Sessions are discovered by `teamName: okstra-<task-id>`, lead is identified by `lead.sessionId`, and other workers are identified by `agentName` (e.g. `claude-worker`, `codex-worker`, `gemini-worker`, `report-writer`).
|
|
369
|
+
- `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` for the lead and every Claude-side worker (Claude worker, Report writer worker, plus the Claude wrappers around Codex/Gemini workers). Sessions are discovered by `teamName: okstra-<task-id>`, lead is identified by `lead.sessionId`, and other workers are identified by `agentName` (e.g. `claude-worker`, `codex-worker`, `gemini-worker`, `report-writer`). **For this `agentName` match to work, Lead MUST set the Agent `name` arg to `<workerId>-worker` on every dispatch** (see [agents SKILL.md Phase 4 — "Agent `name` on dispatch"](../../agents/SKILL.md)); a worker dispatched without `name` carries no `agentName`, so the collector cannot attribute its session and records it `unavailable` (now surfaced as a `usageSummary.unattributedTeamSessions` entry rather than dropped silently).
|
|
370
370
|
- `~/.codex/sessions/Y/M/D/rollout-*.jsonl` for the underlying Codex CLI session (matched by `cwd` and timestamp window of the wrapper subagent). Last `event_msg.token_count.total_token_usage.total_tokens` is the session total.
|
|
371
371
|
- `~/.gemini/tmp/<project>/chats/session-*.json` for the underlying Gemini CLI session. Sum of per-message `tokens.total`.
|
|
372
372
|
|
package/src/install.mjs
CHANGED