@tamng0905/builder-essential-skills 0.1.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/.claude/agents/lead-builder.md +55 -0
- package/.claude/agents/lead-reviewer.md +39 -0
- package/README.md +276 -0
- package/assets/readme-hero.png +0 -0
- package/assets/skill-cards/async-learning-teacher.svg +7 -0
- package/assets/skill-cards/code-standards.svg +7 -0
- package/assets/skill-cards/lead-research.svg +7 -0
- package/assets/skill-cards/lead.svg +7 -0
- package/assets/skill-cards/orwell-writing.svg +7 -0
- package/assets/skill-cards/session-profiler.svg +7 -0
- package/assets/skill-cards/validate-market.svg +7 -0
- package/assets/skill-cards/write-blog.svg +7 -0
- package/bin/builder-essential-skills.js +134 -0
- package/install.ps1 +58 -0
- package/install.sh +63 -0
- package/package.json +24 -0
- package/skills/async-learning-teacher/README.md +50 -0
- package/skills/async-learning-teacher/SKILL.md +192 -0
- package/skills/async-learning-teacher/agents/openai.yaml +4 -0
- package/skills/code-standards/README.md +27 -0
- package/skills/code-standards/SKILL.md +51 -0
- package/skills/lead/README.md +29 -0
- package/skills/lead/SKILL.md +222 -0
- package/skills/lead/config.py +359 -0
- package/skills/lead/dispatch.md +342 -0
- package/skills/lead/loop.md +103 -0
- package/skills/lead/models.json +34 -0
- package/skills/lead/research.md +76 -0
- package/skills/lead/status.ps1 +114 -0
- package/skills/lead/status.sh +110 -0
- package/skills/lead/watchdog.ps1 +90 -0
- package/skills/lead/watchdog.sh +88 -0
- package/skills/lead-research/README.md +27 -0
- package/skills/lead-research/SKILL.md +157 -0
- package/skills/lead-research/tactics.md +150 -0
- package/skills/orwell-writing/README.md +27 -0
- package/skills/orwell-writing/SKILL.md +44 -0
- package/skills/orwell-writing/agents/openai.yaml +4 -0
- package/skills/session-profiler/README.md +25 -0
- package/skills/session-profiler/SKILL.md +103 -0
- package/skills/session-profiler/agents/openai.yaml +4 -0
- package/skills/session-profiler/scripts/requirements.txt +2 -0
- package/skills/session-profiler/scripts/session_profiler/__init__.py +5 -0
- package/skills/session-profiler/scripts/session_profiler/analyses.py +246 -0
- package/skills/session-profiler/scripts/session_profiler/cli.py +77 -0
- package/skills/session-profiler/scripts/session_profiler/dataset.py +369 -0
- package/skills/session-profiler/scripts/session_profiler/discover.py +191 -0
- package/skills/session-profiler/scripts/session_profiler/toc.py +74 -0
- package/skills/session-profiler/scripts/session_profiler/trace.py +146 -0
- package/skills/session-profiler/scripts/sp +20 -0
- package/skills/session-profiler/scripts/tests/codex_home/sessions/2026/01/01/rollout-child.jsonl +7 -0
- package/skills/session-profiler/scripts/tests/codex_home/sessions/2026/01/01/rollout-root.jsonl +10 -0
- package/skills/session-profiler/scripts/tests/fixture/main/subagents/agent-abc.jsonl +2 -0
- package/skills/session-profiler/scripts/tests/fixture/main/subagents/agent-abc.meta.json +1 -0
- package/skills/session-profiler/scripts/tests/fixture/main.jsonl +7 -0
- package/skills/session-profiler/scripts/tests/test_fixture.py +58 -0
- package/skills/validate-market/README.md +27 -0
- package/skills/validate-market/SKILL.md +202 -0
- package/skills/write-blog/README.md +27 -0
- package/skills/write-blog/SKILL.md +142 -0
- package/skills/write-blog/check.sh +52 -0
- package/skills/write-blog/reference/angles.md +51 -0
- package/skills/write-blog/reference/craft.md +94 -0
- package/skills/write-blog/reference/seo.md +116 -0
- package/skills/write-blog/reference/voice.md +64 -0
- package/skills/write-blog/reference/wiring.md +113 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from .discover import codex_family, detect_provider, resolve_session, session_id
|
|
13
|
+
|
|
14
|
+
# Public Claude Opus-tier rates from the source specification, USD per million tokens.
|
|
15
|
+
CLAUDE_PRICE = {"input": 15.0, "cache_read": 1.5, "cache_write": 18.75, "output": 75.0}
|
|
16
|
+
# Public standard API rates per million tokens, verified 2026-07-11 against
|
|
17
|
+
# developers.openai.com/api/docs/models/. Codex subscription billing can differ.
|
|
18
|
+
OPENAI_PRICE = {
|
|
19
|
+
"gpt-5.6-sol": {"input": 5.0, "cache_read": 0.5, "cache_write": 6.25, "output": 30.0},
|
|
20
|
+
"gpt-5.6-terra": {"input": 2.5, "cache_read": 0.25, "cache_write": 3.125, "output": 15.0},
|
|
21
|
+
"gpt-5.6-luna": {"input": 1.0, "cache_read": 0.1, "cache_write": 1.25, "output": 6.0},
|
|
22
|
+
"gpt-5.6": {"input": 5.0, "cache_read": 0.5, "cache_write": 6.25, "output": 30.0},
|
|
23
|
+
"gpt-5.4": {"input": 2.5, "cache_read": 0.25, "cache_write": 3.125, "output": 15.0},
|
|
24
|
+
"gpt-5": {"input": 1.25, "cache_read": 0.125, "cache_write": 1.5625, "output": 10.0},
|
|
25
|
+
}
|
|
26
|
+
OPAQUE = re.compile(r"(?<![A-Za-z0-9])[A-Za-z0-9_+\-/=]{40,}")
|
|
27
|
+
AUTH = re.compile(r"(?i)(authorization|cookie|x-api-key)([\"' ]*[:=][\"' ]*)([^\s,;}]+)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sanitize(value: Any, limit: int = 2000) -> str:
|
|
31
|
+
if value is None:
|
|
32
|
+
return ""
|
|
33
|
+
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
|
|
34
|
+
text = AUTH.sub(r"\1\2[REDACTED]", text)
|
|
35
|
+
text = OPAQUE.sub("[REDACTED_TOKEN]", text)
|
|
36
|
+
return text[:limit]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ts(value: Any) -> pd.Timestamp:
|
|
40
|
+
return pd.to_datetime(value, utc=True, errors="coerce")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _price(provider: str, model: str) -> dict | None:
|
|
44
|
+
if provider == "claude":
|
|
45
|
+
return CLAUDE_PRICE
|
|
46
|
+
for prefix, price in OPENAI_PRICE.items():
|
|
47
|
+
if model == prefix or re.fullmatch(re.escape(prefix) + r"-\d{4}-\d{2}-\d{2}", model):
|
|
48
|
+
return price
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cost(usage: dict, provider: str = "claude", model: str = "") -> float:
|
|
53
|
+
price = _price(provider, model)
|
|
54
|
+
if not price:
|
|
55
|
+
return 0.0
|
|
56
|
+
long_context = provider == "codex" and model.startswith(("gpt-5.4", "gpt-5.6")) and usage.get("input_tokens", 0) + usage.get("cache_read_input_tokens", 0) > 272_000
|
|
57
|
+
input_multiplier = 2.0 if long_context else 1.0
|
|
58
|
+
output_multiplier = 1.5 if long_context else 1.0
|
|
59
|
+
return (
|
|
60
|
+
usage.get("input_tokens", 0) * price["input"] * input_multiplier
|
|
61
|
+
+ usage.get("cache_read_input_tokens", 0) * price["cache_read"] * input_multiplier
|
|
62
|
+
+ usage.get("cache_creation_input_tokens", 0) * price["cache_write"]
|
|
63
|
+
+ usage.get("output_tokens", 0) * price["output"] * output_multiplier
|
|
64
|
+
) / 1_000_000
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _records(path: Path) -> list[dict]:
|
|
68
|
+
rows = []
|
|
69
|
+
with path.open(errors="replace") as fh:
|
|
70
|
+
for line_no, line in enumerate(fh, 1):
|
|
71
|
+
try:
|
|
72
|
+
row = json.loads(line)
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
continue
|
|
75
|
+
row["_line_no"] = line_no
|
|
76
|
+
rows.append(row)
|
|
77
|
+
return rows
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _agent_files(main: Path) -> list[tuple[str, Path, dict]]:
|
|
81
|
+
if detect_provider(main) == "codex":
|
|
82
|
+
family = codex_family(main)
|
|
83
|
+
root_id = family[0][0]
|
|
84
|
+
found = []
|
|
85
|
+
for index, (thread_id, path, session_meta) in enumerate(family):
|
|
86
|
+
spawn = session_meta.get("source", {}).get("subagent", {}).get("thread_spawn", {}) if isinstance(session_meta.get("source"), dict) else {}
|
|
87
|
+
found.append(("main" if index == 0 else thread_id, path, {
|
|
88
|
+
"name": "main" if index == 0 else spawn.get("agent_nickname", thread_id),
|
|
89
|
+
"agentType": "main" if index == 0 else spawn.get("agent_role", "subagent"),
|
|
90
|
+
"description": "",
|
|
91
|
+
"spawnDepth": 0 if index == 0 else spawn.get("depth", 1),
|
|
92
|
+
"parentThreadId": "" if index == 0 else spawn.get("parent_thread_id", root_id),
|
|
93
|
+
"threadId": thread_id,
|
|
94
|
+
"rootThreadId": root_id,
|
|
95
|
+
}))
|
|
96
|
+
return found
|
|
97
|
+
found: list[tuple[str, Path, dict]] = [("main", main, {"name": "main", "agentType": "main", "spawnDepth": 0})]
|
|
98
|
+
side = main.parent / main.stem / "subagents"
|
|
99
|
+
if not side.exists():
|
|
100
|
+
return found
|
|
101
|
+
for path in sorted(side.glob("agent-*.jsonl")):
|
|
102
|
+
meta_path = path.with_suffix(".meta.json")
|
|
103
|
+
try:
|
|
104
|
+
meta = json.loads(meta_path.read_text()) if meta_path.exists() else {}
|
|
105
|
+
except (OSError, json.JSONDecodeError):
|
|
106
|
+
meta = {}
|
|
107
|
+
found.append((path.stem, path, meta))
|
|
108
|
+
return found
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _content_blocks(row: dict) -> list[dict]:
|
|
112
|
+
content = row.get("message", {}).get("content", [])
|
|
113
|
+
if isinstance(content, str):
|
|
114
|
+
return [{"type": "text", "text": content}]
|
|
115
|
+
return content if isinstance(content, list) else []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse(session: str | Path, out_dir: str | Path | None = None) -> Path:
|
|
119
|
+
main = resolve_session(session)
|
|
120
|
+
provider = detect_provider(main)
|
|
121
|
+
parsed_session_id = session_id(main)
|
|
122
|
+
out = Path(out_dir).expanduser() if out_dir else Path(os.environ.get("SESSION_DATA_DIR", Path.home() / ".cache" / "session-profiler" / parsed_session_id))
|
|
123
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
files = _agent_files(main)
|
|
126
|
+
raw_by_agent = {agent_id: _records(path) for agent_id, path, _ in files}
|
|
127
|
+
spawn_owner: dict[str, str] = {}
|
|
128
|
+
for agent_id, rows in raw_by_agent.items():
|
|
129
|
+
for row in rows:
|
|
130
|
+
if provider == "claude":
|
|
131
|
+
for block in _content_blocks(row):
|
|
132
|
+
if block.get("type") == "tool_use" and block.get("name") in {"Agent", "Task"}:
|
|
133
|
+
spawn_owner[block.get("id", "")] = agent_id
|
|
134
|
+
id_map = {meta.get("threadId"): agent_id for agent_id, _, meta in files}
|
|
135
|
+
|
|
136
|
+
agents = []
|
|
137
|
+
events: list[dict] = []
|
|
138
|
+
for agent_id, path, meta in files:
|
|
139
|
+
rows = raw_by_agent[agent_id]
|
|
140
|
+
timestamps = [_ts(r.get("timestamp")) for r in rows]
|
|
141
|
+
timestamps = [t for t in timestamps if not pd.isna(t)]
|
|
142
|
+
parent_id = spawn_owner.get(meta.get("toolUseId", ""), "" if agent_id == "main" else "main")
|
|
143
|
+
spawn_tool_id = meta.get("toolUseId", "")
|
|
144
|
+
if provider == "codex" and agent_id != "main":
|
|
145
|
+
parent_id = id_map.get(meta.get("parentThreadId"), "main")
|
|
146
|
+
child_thread = meta.get("threadId", "")
|
|
147
|
+
for row in raw_by_agent.get(parent_id, []):
|
|
148
|
+
payload = row.get("payload", {})
|
|
149
|
+
if payload.get("type") in {"custom_tool_call_output", "function_call_output"} and child_thread in json.dumps(payload.get("output", ""), default=str):
|
|
150
|
+
spawn_tool_id = payload.get("call_id", "")
|
|
151
|
+
break
|
|
152
|
+
description = meta.get("description", "")
|
|
153
|
+
if provider == "codex" and not description:
|
|
154
|
+
description = next((sanitize(row.get("payload", {}).get("message", ""), 200) for row in rows if row.get("type") == "event_msg" and row.get("payload", {}).get("type") == "user_message"), "")
|
|
155
|
+
agent = {
|
|
156
|
+
"id": agent_id,
|
|
157
|
+
"name": meta.get("name") or agent_id,
|
|
158
|
+
"agent_type": meta.get("agentType") or ("main" if agent_id == "main" else "subagent"),
|
|
159
|
+
"description": description,
|
|
160
|
+
"spawn_depth": int(meta.get("spawnDepth", 0 if agent_id == "main" else 1)),
|
|
161
|
+
"spawn_tool_use_id": spawn_tool_id,
|
|
162
|
+
"parent_agent_id": parent_id,
|
|
163
|
+
"provider": provider,
|
|
164
|
+
"source": str(path),
|
|
165
|
+
"start_ts": timestamps[0].isoformat() if timestamps else None,
|
|
166
|
+
"end_ts": timestamps[-1].isoformat() if timestamps else None,
|
|
167
|
+
}
|
|
168
|
+
agents.append(agent)
|
|
169
|
+
if provider == "codex":
|
|
170
|
+
_parse_codex_agent(rows, agent, events)
|
|
171
|
+
else:
|
|
172
|
+
_parse_claude_agent(rows, agent, events)
|
|
173
|
+
|
|
174
|
+
df = pd.DataFrame(events)
|
|
175
|
+
if not df.empty:
|
|
176
|
+
df["ts"] = pd.to_datetime(df["ts"], utc=True)
|
|
177
|
+
df["end_ts"] = pd.to_datetime(df["end_ts"], utc=True)
|
|
178
|
+
df = df.sort_values(["ts", "agent_id", "line_no"], na_position="last").reset_index(drop=True)
|
|
179
|
+
df.to_parquet(out / "events.parquet", index=False)
|
|
180
|
+
df.to_csv(out / "events.csv", index=False)
|
|
181
|
+
(out / "agents.json").write_text(json.dumps({"provider": provider, "session_id": parsed_session_id, "main": str(main), "agents": agents}, indent=2))
|
|
182
|
+
(out / ".session-data-dir").write_text(str(out.resolve()))
|
|
183
|
+
state = Path.home() / ".cache" / "session-profiler" / "last-work-dir"
|
|
184
|
+
state.parent.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
state.write_text(str(out.resolve()))
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _parse_claude_agent(rows: list[dict], agent: dict, events: list[dict]) -> None:
|
|
190
|
+
aid = agent["id"]
|
|
191
|
+
base = {"provider": "claude", "agent_id": aid, "agent_name": agent["name"], "agent_type": agent["agent_type"], "spawn_depth": agent["spawn_depth"]}
|
|
192
|
+
tool_starts: dict[str, dict] = {}
|
|
193
|
+
tool_uses_by_message: defaultdict[str, list[dict]] = defaultdict(list)
|
|
194
|
+
messages: defaultdict[str, list[tuple[pd.Timestamp, dict]]] = defaultdict(list)
|
|
195
|
+
|
|
196
|
+
for row in rows:
|
|
197
|
+
ts = _ts(row.get("timestamp"))
|
|
198
|
+
if pd.isna(ts):
|
|
199
|
+
continue
|
|
200
|
+
typ = row.get("type")
|
|
201
|
+
msg = row.get("message", {})
|
|
202
|
+
mid = msg.get("id", "")
|
|
203
|
+
if typ == "assistant" and mid:
|
|
204
|
+
messages[mid].append((ts, row))
|
|
205
|
+
if typ == "user" and not row.get("toolUseResult"):
|
|
206
|
+
texts = [b.get("text", "") for b in _content_blocks(row) if b.get("type") == "text"]
|
|
207
|
+
if not texts and isinstance(msg.get("content"), str):
|
|
208
|
+
texts = [msg["content"]]
|
|
209
|
+
text = "\n".join(texts).strip()
|
|
210
|
+
if text:
|
|
211
|
+
events.append({**base, "ts": ts, "end_ts": ts, "duration_ms": 0.0, "event_type": "user_prompt", "message_id": "", "tool_use_id": "", "tool_name": "", "model": "", "text": sanitize(text), "input_preview": "", "output_preview": "", "is_error": False, "concurrent": False, "input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 0, "estimated_cost_usd": 0.0, "source": agent["source"], "line_no": row["_line_no"]})
|
|
212
|
+
for block in _content_blocks(row):
|
|
213
|
+
btype = block.get("type", "")
|
|
214
|
+
common = {**base, "ts": ts, "end_ts": ts, "duration_ms": 0.0, "message_id": mid, "model": msg.get("model", ""), "source": agent["source"], "line_no": row["_line_no"], "input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 0, "estimated_cost_usd": 0.0, "is_error": False, "concurrent": False}
|
|
215
|
+
if typ == "assistant" and btype in {"text", "thinking"}:
|
|
216
|
+
events.append({**common, "event_type": "assistant_text" if btype == "text" else "thinking", "tool_use_id": "", "tool_name": "", "text": sanitize(block.get("text") or block.get("thinking")), "input_preview": "", "output_preview": ""})
|
|
217
|
+
elif typ == "assistant" and btype == "tool_use":
|
|
218
|
+
event = {**common, "event_type": "tool", "tool_use_id": block.get("id", ""), "tool_name": block.get("name", ""), "text": "", "input_preview": sanitize(block.get("input")), "output_preview": ""}
|
|
219
|
+
events.append(event)
|
|
220
|
+
tool_starts[event["tool_use_id"]] = event
|
|
221
|
+
tool_uses_by_message[mid].append(event)
|
|
222
|
+
elif typ == "user" and btype == "tool_result":
|
|
223
|
+
tool_id = block.get("tool_use_id", "")
|
|
224
|
+
error = bool(block.get("is_error") or row.get("toolUseResult", {}).get("isError"))
|
|
225
|
+
output = block.get("content", row.get("toolUseResult", ""))
|
|
226
|
+
events.append({**common, "event_type": "tool_result", "tool_use_id": tool_id, "tool_name": "", "text": "", "input_preview": "", "output_preview": sanitize(output), "is_error": error})
|
|
227
|
+
if tool_id in tool_starts:
|
|
228
|
+
start = tool_starts[tool_id]
|
|
229
|
+
start["end_ts"] = ts
|
|
230
|
+
start["duration_ms"] = max(0.0, (ts - start["ts"]).total_seconds() * 1000)
|
|
231
|
+
start["output_preview"] = sanitize(output)
|
|
232
|
+
start["is_error"] = error
|
|
233
|
+
|
|
234
|
+
for group in tool_uses_by_message.values():
|
|
235
|
+
if len(group) > 1:
|
|
236
|
+
for event in group:
|
|
237
|
+
event["concurrent"] = True
|
|
238
|
+
|
|
239
|
+
for mid, snapshots in messages.items():
|
|
240
|
+
snapshots.sort(key=lambda pair: (pair[0], pair[1]["_line_no"]))
|
|
241
|
+
first_ts, first_row = snapshots[0]
|
|
242
|
+
last_ts, last_row = snapshots[-1]
|
|
243
|
+
usage = last_row.get("message", {}).get("usage", {}) or {}
|
|
244
|
+
events.append({**base, "ts": first_ts, "end_ts": last_ts, "duration_ms": max(0.0, (last_ts - first_ts).total_seconds() * 1000), "event_type": "inference", "message_id": mid, "tool_use_id": "", "tool_name": "", "model": last_row.get("message", {}).get("model", ""), "text": "", "input_preview": "", "output_preview": "", "is_error": False, "concurrent": False, "input_tokens": int(usage.get("input_tokens", 0) or 0), "cache_read_input_tokens": int(usage.get("cache_read_input_tokens", 0) or 0), "cache_creation_input_tokens": int(usage.get("cache_creation_input_tokens", 0) or 0), "output_tokens": int(usage.get("output_tokens", 0) or 0), "estimated_cost_usd": _cost(usage), "source": agent["source"], "line_no": first_row["_line_no"]})
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _codex_is_error(value: Any) -> bool:
|
|
248
|
+
if isinstance(value, str):
|
|
249
|
+
try:
|
|
250
|
+
return _codex_is_error(json.loads(value))
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
return bool(re.search(r"(?i)(?:process exited with code|exit code\s*[:=]?)\s*[1-9]\d*", value))
|
|
253
|
+
if isinstance(value, list):
|
|
254
|
+
return any(_codex_is_error(item) for item in value)
|
|
255
|
+
if not isinstance(value, dict):
|
|
256
|
+
return False
|
|
257
|
+
if value.get("is_error") or value.get("isError") or value.get("success") is False:
|
|
258
|
+
return True
|
|
259
|
+
if value.get("exit_code") not in (None, 0) or value.get("exitCode") not in (None, 0):
|
|
260
|
+
return True
|
|
261
|
+
if str(value.get("status", "")).lower() in {"error", "failed", "failure"}:
|
|
262
|
+
return True
|
|
263
|
+
return any(_codex_is_error(item) for item in value.values() if isinstance(item, (dict, list)))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _parse_codex_agent(rows: list[dict], agent: dict, events: list[dict]) -> None:
|
|
267
|
+
base = {"provider": "codex", "agent_id": agent["id"], "agent_name": agent["name"], "agent_type": agent["agent_type"], "spawn_depth": agent["spawn_depth"]}
|
|
268
|
+
models: dict[str, str] = {}
|
|
269
|
+
for row in rows:
|
|
270
|
+
if row.get("type") == "turn_context":
|
|
271
|
+
payload = row.get("payload", {})
|
|
272
|
+
models[payload.get("turn_id", "")] = payload.get("model", "")
|
|
273
|
+
current_turn = ""
|
|
274
|
+
inference_start: dict[str, pd.Timestamp] = {}
|
|
275
|
+
tool_starts: dict[str, dict] = {}
|
|
276
|
+
pending_tools: list[dict] = []
|
|
277
|
+
response_index = 0
|
|
278
|
+
|
|
279
|
+
def common(ts: pd.Timestamp, row: dict, turn_id: str) -> dict:
|
|
280
|
+
return {**base, "ts": ts, "end_ts": ts, "duration_ms": 0.0, "message_id": turn_id, "model": models.get(turn_id, ""), "source": agent["source"], "line_no": row["_line_no"], "input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 0, "estimated_cost_usd": 0.0, "is_error": False, "concurrent": False}
|
|
281
|
+
|
|
282
|
+
for row in rows:
|
|
283
|
+
ts = _ts(row.get("timestamp"))
|
|
284
|
+
if pd.isna(ts):
|
|
285
|
+
continue
|
|
286
|
+
payload = row.get("payload", {})
|
|
287
|
+
row_type = row.get("type")
|
|
288
|
+
payload_type = payload.get("type", "")
|
|
289
|
+
metadata = payload.get("internal_chat_message_metadata_passthrough", {}) or {}
|
|
290
|
+
turn_id = metadata.get("turn_id") or payload.get("turn_id") or current_turn
|
|
291
|
+
if row_type == "event_msg" and payload_type == "task_started":
|
|
292
|
+
current_turn = turn_id
|
|
293
|
+
inference_start[turn_id] = ts
|
|
294
|
+
continue
|
|
295
|
+
if row_type == "turn_context":
|
|
296
|
+
current_turn = payload.get("turn_id", current_turn)
|
|
297
|
+
continue
|
|
298
|
+
if row_type == "event_msg" and payload_type == "user_message":
|
|
299
|
+
text = payload.get("message", "")
|
|
300
|
+
if text:
|
|
301
|
+
events.append({**common(ts, row, turn_id), "event_type": "user_prompt", "tool_use_id": "", "tool_name": "", "text": sanitize(text), "input_preview": "", "output_preview": ""})
|
|
302
|
+
continue
|
|
303
|
+
if row_type == "response_item" and payload_type == "message":
|
|
304
|
+
if payload.get("role") != "assistant":
|
|
305
|
+
continue
|
|
306
|
+
text = "\n".join(block.get("text", "") for block in payload.get("content", []) if block.get("type") in {"output_text", "text"})
|
|
307
|
+
if text:
|
|
308
|
+
events.append({**common(ts, row, turn_id), "event_type": "assistant_text", "tool_use_id": "", "tool_name": "", "text": sanitize(text), "input_preview": "", "output_preview": ""})
|
|
309
|
+
continue
|
|
310
|
+
if row_type == "response_item" and payload_type == "reasoning":
|
|
311
|
+
summary = payload.get("summary", [])
|
|
312
|
+
text = "\n".join(item.get("text", "") if isinstance(item, dict) else str(item) for item in summary)
|
|
313
|
+
events.append({**common(ts, row, turn_id), "event_type": "thinking", "tool_use_id": "", "tool_name": "", "text": sanitize(text), "input_preview": "", "output_preview": ""})
|
|
314
|
+
continue
|
|
315
|
+
if row_type == "response_item" and payload_type in {"custom_tool_call", "function_call", "local_shell_call", "web_search_call"}:
|
|
316
|
+
tool_id = payload.get("call_id") or payload.get("id", "")
|
|
317
|
+
tool_input = payload.get("input", payload.get("arguments", payload.get("action", "")))
|
|
318
|
+
event = {**common(ts, row, turn_id), "event_type": "tool", "tool_use_id": tool_id, "tool_name": payload.get("name") or payload_type.removesuffix("_call"), "text": "", "input_preview": sanitize(tool_input), "output_preview": ""}
|
|
319
|
+
events.append(event)
|
|
320
|
+
tool_starts[tool_id] = event
|
|
321
|
+
pending_tools.append(event)
|
|
322
|
+
continue
|
|
323
|
+
if row_type == "response_item" and payload_type in {"custom_tool_call_output", "function_call_output", "local_shell_call_output"}:
|
|
324
|
+
tool_id = payload.get("call_id", "")
|
|
325
|
+
output = payload.get("output", "")
|
|
326
|
+
error = _codex_is_error(output)
|
|
327
|
+
events.append({**common(ts, row, turn_id), "event_type": "tool_result", "tool_use_id": tool_id, "tool_name": "", "text": "", "input_preview": "", "output_preview": sanitize(output), "is_error": error})
|
|
328
|
+
if tool_id in tool_starts:
|
|
329
|
+
start = tool_starts[tool_id]
|
|
330
|
+
start["end_ts"] = ts
|
|
331
|
+
start["duration_ms"] = max(0.0, (ts - start["ts"]).total_seconds() * 1000)
|
|
332
|
+
start["output_preview"] = sanitize(output)
|
|
333
|
+
start["is_error"] = error
|
|
334
|
+
inference_start[turn_id] = ts
|
|
335
|
+
continue
|
|
336
|
+
if row_type == "event_msg" and payload_type == "token_count":
|
|
337
|
+
if len(pending_tools) > 1:
|
|
338
|
+
for event in pending_tools:
|
|
339
|
+
event["concurrent"] = True
|
|
340
|
+
pending_tools = []
|
|
341
|
+
usage = payload.get("info", {}).get("last_token_usage", {}) or {}
|
|
342
|
+
if not usage or not turn_id or not any(int(usage.get(key, 0) or 0) for key in ("input_tokens", "cached_input_tokens", "output_tokens")):
|
|
343
|
+
continue
|
|
344
|
+
response_index += 1
|
|
345
|
+
cached = int(usage.get("cached_input_tokens", 0) or 0)
|
|
346
|
+
normalized = {"input_tokens": max(0, int(usage.get("input_tokens", 0) or 0) - cached), "cache_read_input_tokens": cached, "cache_creation_input_tokens": 0, "output_tokens": int(usage.get("output_tokens", 0) or 0)}
|
|
347
|
+
start = inference_start.get(turn_id, ts)
|
|
348
|
+
model = models.get(turn_id, "")
|
|
349
|
+
events.append({**common(start, row, turn_id), "end_ts": ts, "duration_ms": max(0.0, (ts - start).total_seconds() * 1000), "event_type": "inference", "message_id": f"{turn_id}:{response_index}", "tool_use_id": "", "tool_name": "", "model": model, "text": "", "input_preview": "", "output_preview": "", **normalized, "estimated_cost_usd": _cost(normalized, "codex", model)})
|
|
350
|
+
inference_start[turn_id] = ts
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def work_dir(value: str | Path | None = None) -> Path:
|
|
354
|
+
if value:
|
|
355
|
+
return Path(value).expanduser()
|
|
356
|
+
if os.environ.get("SESSION_DATA_DIR"):
|
|
357
|
+
return Path(os.environ["SESSION_DATA_DIR"]).expanduser()
|
|
358
|
+
state = Path.home() / ".cache" / "session-profiler" / "last-work-dir"
|
|
359
|
+
if state.exists():
|
|
360
|
+
return Path(state.read_text().strip())
|
|
361
|
+
raise FileNotFoundError("No parsed session. Run `sp parse <session>` first or set SESSION_DATA_DIR.")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def load(value: str | Path | None = None) -> pd.DataFrame:
|
|
365
|
+
return pd.read_parquet(work_dir(value) / "events.parquet")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def load_agents(value: str | Path | None = None) -> dict:
|
|
369
|
+
return json.loads((work_dir(value) / "agents.json").read_text())
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def claude_projects_root() -> Path:
|
|
10
|
+
return Path.home() / ".claude" / "projects"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def codex_roots() -> list[Path]:
|
|
14
|
+
root = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")).expanduser()
|
|
15
|
+
return [root / "sessions", root / "archived_sessions"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def project_dirs() -> list[Path]:
|
|
19
|
+
root = claude_projects_root()
|
|
20
|
+
paths = [p for p in root.iterdir() if p.is_dir()] if root.exists() else []
|
|
21
|
+
paths.extend(p for p in codex_roots() if p.exists())
|
|
22
|
+
return sorted(paths, key=lambda p: p.stat().st_mtime, reverse=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def detect_provider(path: Path) -> str:
|
|
26
|
+
try:
|
|
27
|
+
first = json.loads(path.open(errors="replace").readline())
|
|
28
|
+
return "codex" if first.get("type") == "session_meta" else "claude"
|
|
29
|
+
except (OSError, json.JSONDecodeError):
|
|
30
|
+
return "claude"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _session_meta(path: Path) -> dict:
|
|
34
|
+
try:
|
|
35
|
+
with path.open(errors="replace") as fh:
|
|
36
|
+
first = json.loads(fh.readline())
|
|
37
|
+
return first.get("payload", {}) if first.get("type") == "session_meta" else {}
|
|
38
|
+
except (OSError, json.JSONDecodeError):
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def session_id(path: Path) -> str:
|
|
43
|
+
return _session_meta(path).get("id") or path.stem
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def codex_transcripts() -> list[Path]:
|
|
47
|
+
paths = []
|
|
48
|
+
for root in codex_roots():
|
|
49
|
+
if root.exists():
|
|
50
|
+
paths.extend(root.rglob("rollout-*.jsonl"))
|
|
51
|
+
return sorted(paths, key=lambda p: p.stat().st_mtime, reverse=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def codex_index() -> dict[str, tuple[Path, dict]]:
|
|
55
|
+
result = {}
|
|
56
|
+
for path in codex_transcripts():
|
|
57
|
+
meta = _session_meta(path)
|
|
58
|
+
if meta.get("id"):
|
|
59
|
+
result[meta["id"]] = (path, meta)
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def codex_family(path: Path) -> list[tuple[str, Path, dict]]:
|
|
64
|
+
"""Return a Codex rollout and all recursively spawned subagent rollouts."""
|
|
65
|
+
index = codex_index()
|
|
66
|
+
root_id = session_id(path)
|
|
67
|
+
children: dict[str, list[tuple[str, Path, dict]]] = {}
|
|
68
|
+
for thread_id, (candidate, meta) in index.items():
|
|
69
|
+
spawn = meta.get("source", {}).get("subagent", {}).get("thread_spawn", {}) if isinstance(meta.get("source"), dict) else {}
|
|
70
|
+
parent = spawn.get("parent_thread_id")
|
|
71
|
+
if parent:
|
|
72
|
+
children.setdefault(parent, []).append((thread_id, candidate, meta))
|
|
73
|
+
found: list[tuple[str, Path, dict]] = [(root_id, path, _session_meta(path))]
|
|
74
|
+
queue = [root_id]
|
|
75
|
+
while queue:
|
|
76
|
+
parent = queue.pop(0)
|
|
77
|
+
for item in children.get(parent, []):
|
|
78
|
+
found.append(item)
|
|
79
|
+
queue.append(item[0])
|
|
80
|
+
return found
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main_transcripts(project_dir: Path) -> list[Path]:
|
|
84
|
+
if not project_dir.exists():
|
|
85
|
+
return []
|
|
86
|
+
if project_dir in codex_roots() or ".codex/sessions" in str(project_dir) or ".codex/archived_sessions" in str(project_dir):
|
|
87
|
+
paths = project_dir.rglob("rollout-*.jsonl")
|
|
88
|
+
return sorted((p for p in paths if not _codex_parent_id(p)), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
89
|
+
return sorted((p for p in project_dir.glob("*.jsonl") if p.is_file()), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _codex_parent_id(path: Path) -> str:
|
|
93
|
+
source = _session_meta(path).get("source", {})
|
|
94
|
+
if not isinstance(source, dict):
|
|
95
|
+
return ""
|
|
96
|
+
return source.get("subagent", {}).get("thread_spawn", {}).get("parent_thread_id", "")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_codex_subagent(path: Path) -> bool:
|
|
100
|
+
return bool(_codex_parent_id(path))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def resolve_session(value: str | Path) -> Path:
|
|
104
|
+
candidate = Path(value).expanduser()
|
|
105
|
+
if candidate.is_file():
|
|
106
|
+
return candidate.resolve()
|
|
107
|
+
matches = find_sessions(str(value))
|
|
108
|
+
if not matches:
|
|
109
|
+
raise FileNotFoundError(f"No Claude Code or Codex session matches {value!r}")
|
|
110
|
+
if len(matches) > 1:
|
|
111
|
+
shown = "\n".join(f" {session_id(p)} {p}" for p in matches[:12])
|
|
112
|
+
raise ValueError(f"Session prefix is ambiguous:\n{shown}")
|
|
113
|
+
return matches[0]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_sessions(prefix: str) -> list[Path]:
|
|
117
|
+
claude = [p for project in project_dirs() if ".claude/projects" in str(project) for p in main_transcripts(project) if p.stem.startswith(prefix)]
|
|
118
|
+
codex = [p for p in codex_transcripts() if session_id(p).startswith(prefix) or p.stem.startswith(prefix)]
|
|
119
|
+
return claude + codex
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _first_prompt(path: Path, limit: int = 100) -> str:
|
|
123
|
+
provider = detect_provider(path)
|
|
124
|
+
try:
|
|
125
|
+
with path.open(errors="replace") as fh:
|
|
126
|
+
for line in fh:
|
|
127
|
+
row = json.loads(line)
|
|
128
|
+
if provider == "codex":
|
|
129
|
+
payload = row.get("payload", {})
|
|
130
|
+
if row.get("type") != "event_msg" or payload.get("type") != "user_message":
|
|
131
|
+
continue
|
|
132
|
+
text = payload.get("message", "")
|
|
133
|
+
else:
|
|
134
|
+
if row.get("type") != "user" or row.get("toolUseResult"):
|
|
135
|
+
continue
|
|
136
|
+
content = row.get("message", {}).get("content", "")
|
|
137
|
+
text = content if isinstance(content, str) else " ".join(x.get("text", "") for x in content if x.get("type") == "text")
|
|
138
|
+
if text.strip():
|
|
139
|
+
return " ".join(text.split())[:limit]
|
|
140
|
+
except (OSError, json.JSONDecodeError):
|
|
141
|
+
pass
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def session_info(path: Path) -> dict:
|
|
146
|
+
provider = detect_provider(path)
|
|
147
|
+
agents = []
|
|
148
|
+
if provider == "codex":
|
|
149
|
+
root_id = session_id(path)
|
|
150
|
+
for thread_id, transcript, meta in codex_family(path)[1:]:
|
|
151
|
+
spawn = meta.get("source", {}).get("subagent", {}).get("thread_spawn", {})
|
|
152
|
+
model, start, end = _transcript_shape(transcript)
|
|
153
|
+
agents.append({"id": thread_id, "name": spawn.get("agent_nickname") or thread_id, "agentType": spawn.get("agent_role", "subagent"), "description": _first_prompt(transcript, 200), "spawnDepth": spawn.get("depth", 1), "parentThreadId": spawn.get("parent_thread_id", root_id), "model": model, "start_ts": start, "end_ts": end})
|
|
154
|
+
else:
|
|
155
|
+
side = path.parent / path.stem / "subagents"
|
|
156
|
+
for meta_path in sorted(side.glob("agent-*.meta.json")) if side.exists() else []:
|
|
157
|
+
try:
|
|
158
|
+
meta = json.loads(meta_path.read_text())
|
|
159
|
+
except (OSError, json.JSONDecodeError):
|
|
160
|
+
meta = {}
|
|
161
|
+
meta["id"] = meta_path.name.removesuffix(".meta.json")
|
|
162
|
+
model, start, end = _transcript_shape(meta_path.with_suffix("").with_suffix(".jsonl"))
|
|
163
|
+
meta.update({"model": model, "start_ts": start, "end_ts": end})
|
|
164
|
+
agents.append(meta)
|
|
165
|
+
model, start, end = _transcript_shape(path)
|
|
166
|
+
span_seconds = None
|
|
167
|
+
if start and end:
|
|
168
|
+
span_seconds = (datetime.fromisoformat(end.replace("Z", "+00:00")) - datetime.fromisoformat(start.replace("Z", "+00:00"))).total_seconds()
|
|
169
|
+
return {"provider": provider, "session_id": session_id(path), "path": str(path), "bytes": path.stat().st_size, "modified": path.stat().st_mtime, "model": model, "start_ts": start, "end_ts": end, "span_seconds": span_seconds, "first_prompt": _first_prompt(path), "subagents": agents}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _transcript_shape(path: Path) -> tuple[str, str | None, str | None]:
|
|
173
|
+
model = ""
|
|
174
|
+
start = end = None
|
|
175
|
+
if not path.exists():
|
|
176
|
+
return model, start, end
|
|
177
|
+
try:
|
|
178
|
+
with path.open(errors="replace") as fh:
|
|
179
|
+
for line in fh:
|
|
180
|
+
row = json.loads(line)
|
|
181
|
+
stamp = row.get("timestamp")
|
|
182
|
+
if stamp:
|
|
183
|
+
start = start or stamp
|
|
184
|
+
end = stamp
|
|
185
|
+
if not model:
|
|
186
|
+
model = row.get("message", {}).get("model", "")
|
|
187
|
+
if row.get("type") == "turn_context":
|
|
188
|
+
model = row.get("payload", {}).get("model", "")
|
|
189
|
+
except (OSError, json.JSONDecodeError):
|
|
190
|
+
pass
|
|
191
|
+
return model, start, end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .dataset import load, load_agents, work_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def digest(data_dir=None) -> str:
|
|
12
|
+
df = load(data_dir)
|
|
13
|
+
part = df[(df.agent_id == "main") & df.event_type.isin(["user_prompt", "assistant_text", "tool"])]
|
|
14
|
+
lines = []
|
|
15
|
+
for _, row in part.sort_values("ts").iterrows():
|
|
16
|
+
if row.event_type == "tool" and row.tool_name not in {"Agent", "Task"} and "spawn_agent" not in str(row.tool_name):
|
|
17
|
+
continue
|
|
18
|
+
body = row.text if row.event_type != "tool" else f"spawn {row.tool_name}: {row.input_preview}"
|
|
19
|
+
body = " ".join(str(body).split())[:1000]
|
|
20
|
+
if body:
|
|
21
|
+
lines.append(f"[{row.ts.isoformat()}] {row.event_type}: {body}")
|
|
22
|
+
return "\n".join(lines)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _markdown(data: dict) -> str:
|
|
26
|
+
lines = ["# Session table of contents", ""]
|
|
27
|
+
for phase in data.get("phases", []):
|
|
28
|
+
lines.append(f"## {phase.get('title', 'Phase')}")
|
|
29
|
+
if phase.get("summary"):
|
|
30
|
+
lines.append(phase["summary"])
|
|
31
|
+
lines.append("")
|
|
32
|
+
for step in phase.get("steps", []):
|
|
33
|
+
lines.append(f"- {step.get('title', 'Step')}")
|
|
34
|
+
for sub in step.get("substeps", []):
|
|
35
|
+
lines.append(f" - {sub}")
|
|
36
|
+
lines.append("")
|
|
37
|
+
return "\n".join(lines)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create(data_dir=None, dry_run=False, model=None, engine="auto") -> str:
|
|
41
|
+
out = work_dir(data_dir)
|
|
42
|
+
source = digest(out)
|
|
43
|
+
prompt = """Write a chronological, hierarchical table of contents for this agent coding session.
|
|
44
|
+
Choose the number of phases from the work itself. Each phase needs title, summary, start_ts,
|
|
45
|
+
end_ts, and steps. Each step needs title, start_ts, end_ts, and optional substeps (strings).
|
|
46
|
+
Use concrete actions, not generic labels. Return JSON only: {\"phases\":[...]}.
|
|
47
|
+
|
|
48
|
+
SESSION DIGEST
|
|
49
|
+
""" + source
|
|
50
|
+
(out / "toc-prompt.txt").write_text(prompt)
|
|
51
|
+
if dry_run:
|
|
52
|
+
return prompt
|
|
53
|
+
provider = load_agents(out).get("provider", "claude")
|
|
54
|
+
selected = provider if engine == "auto" else engine
|
|
55
|
+
if selected == "codex":
|
|
56
|
+
result_file = out / "toc-response.txt"
|
|
57
|
+
command = ["codex", "exec", "--ephemeral", "--sandbox", "read-only", "--skip-git-repo-check", "--color", "never", "-o", str(result_file)]
|
|
58
|
+
if model:
|
|
59
|
+
command.extend(["--model", model])
|
|
60
|
+
command.append(prompt)
|
|
61
|
+
proc = subprocess.run(command, text=True, capture_output=True)
|
|
62
|
+
raw = result_file.read_text().strip() if result_file.exists() else proc.stdout.strip()
|
|
63
|
+
else:
|
|
64
|
+
command = ["claude", "-p", "--model", model or "sonnet", prompt]
|
|
65
|
+
proc = subprocess.run(command, text=True, capture_output=True)
|
|
66
|
+
raw = proc.stdout.strip()
|
|
67
|
+
if proc.returncode:
|
|
68
|
+
raise RuntimeError(proc.stderr.strip() or f"{selected} TOC generation failed")
|
|
69
|
+
raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw)
|
|
70
|
+
data = json.loads(raw)
|
|
71
|
+
(out / "toc.json").write_text(json.dumps(data, indent=2))
|
|
72
|
+
md = _markdown(data)
|
|
73
|
+
(out / "toc.md").write_text(md)
|
|
74
|
+
return md
|