hstack 0.16.0 → 0.18.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/CHANGELOG.md +53 -0
- package/README.md +76 -163
- package/VERSION +1 -1
- package/dist/commands/doctor.js +18 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +7 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.js +9 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/descriptions.js +10 -17
- package/dist/lib/descriptions.js.map +1 -1
- package/dist/lib/wire.js +166 -138
- package/dist/lib/wire.js.map +1 -1
- package/dist/manifest.js +41 -29
- package/dist/manifest.js.map +1 -1
- package/package.json +3 -4
- package/template/.claude/agents/adversarial-reviewer.md +44 -50
- package/template/.claude/agents/app-architect.md +44 -96
- package/template/.claude/agents/data-architect.md +46 -89
- package/template/.claude/agents/test-strategist.md +39 -68
- package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -73
- package/template/.claude/skills/hstack-adversarial-review/SKILL.md +46 -152
- package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +89 -103
- package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -68
- package/template/.claude/skills/hstack-commit/SKILL.md +27 -68
- package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -56
- package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
- package/template/.claude/skills/hstack-story/SKILL.md +58 -0
- package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
- package/template/.claude/skills/hstack-wrap/SKILL.md +50 -0
- package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
- package/template/KERNEL.md +75 -318
- package/template/templates/adr.md +2 -18
- package/template/templates/app-architecture.md +38 -50
- package/template/templates/code-standards.md +107 -0
- package/template/templates/data-architecture.md +38 -89
- package/template/templates/eslint-clean-code.mjs +322 -0
- package/template/templates/infrastructure.md +54 -148
- package/template/templates/roadmap.md +6 -12
- package/template/templates/story.md +7 -13
- package/template/templates/tech-debt.md +2 -37
- package/template/templates/tech-stack.md +2 -12
- package/template/.claude/agents/data-specialist.md +0 -81
- package/template/.claude/agents/implementer.md +0 -97
- package/template/.claude/agents/kernel-fit-analyst.md +0 -129
- package/template/.claude/agents/planner.md +0 -75
- package/template/.claude/agents/product-discovery.md +0 -129
- package/template/.claude/agents/product-manager.md +0 -76
- package/template/.claude/agents/researcher.md +0 -87
- package/template/.claude/agents/security-reviewer.md +0 -88
- package/template/.claude/agents/spec-author.md +0 -89
- package/template/.claude/agents/stack-architect.md +0 -108
- package/template/.claude/agents/ui-ux-briefer.md +0 -75
- package/template/.claude/agents/verifier.md +0 -81
- package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
- package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
- package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
- package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
- package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
- package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
- package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
- package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
- package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
- package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
- package/template/.claude/skills/hstack-help/SKILL.md +0 -153
- package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
- package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
- package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
- package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
- package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
- package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
- package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
- package/template/.claude/skills/hstack-research/SKILL.md +0 -115
- package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
- package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
- package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
- package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
- package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
- package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
- package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
- package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
- package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
- package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
- package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
- package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
- package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
- package/template/scripts/compute-merge-readiness.mjs +0 -780
- package/template/scripts/coord/coord_scan.py +0 -681
- package/template/scripts/run-gates.sh +0 -388
- package/template/scripts/telemetry/__init__.py +0 -6
- package/template/scripts/telemetry/insights/__init__.py +0 -0
- package/template/scripts/telemetry/insights/contract_drift.py +0 -137
- package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
- package/template/scripts/telemetry/insights/overengineering.py +0 -115
- package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
- package/template/scripts/telemetry/insights/token_economics.py +0 -302
- package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
- package/template/scripts/telemetry/parsers/__init__.py +0 -0
- package/template/scripts/telemetry/parsers/bodies.py +0 -87
- package/template/scripts/telemetry/parsers/commits.py +0 -219
- package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
- package/template/scripts/telemetry/parsers/sidecars.py +0 -61
- package/template/scripts/telemetry/parsers/transcripts.py +0 -294
- package/template/scripts/telemetry/render.py +0 -460
- package/template/scripts/telemetry/report.py +0 -150
- package/template/scripts/telemetry/run_kernel_fit.py +0 -95
- package/template/scripts/telemetry/session_id.py +0 -139
- package/template/scripts/validate-spec.mjs +0 -3303
- package/template/templates/adversarial-review.md +0 -54
- package/template/templates/bootstrap.md +0 -105
- package/template/templates/change-spec.md +0 -82
- package/template/templates/ci-cd.md +0 -41
- package/template/templates/coord-message.md +0 -56
- package/template/templates/data-review.md +0 -54
- package/template/templates/discovery/brainstorm.md +0 -69
- package/template/templates/discovery/forcing-questions.md +0 -76
- package/template/templates/discovery/project-brief.md +0 -53
- package/template/templates/figma-handoff.md +0 -38
- package/template/templates/glossary.md +0 -20
- package/template/templates/hardening-checklist.md +0 -73
- package/template/templates/incident-runbook.md +0 -57
- package/template/templates/kernel-fit-finding.md +0 -63
- package/template/templates/kernel-fit-flag.md +0 -52
- package/template/templates/module-spec.md +0 -49
- package/template/templates/persona.md +0 -38
- package/template/templates/plan.md +0 -53
- package/template/templates/product-brief.md +0 -56
- package/template/templates/security-review.md +0 -63
- package/template/templates/telemetry-sidecar.md +0 -227
- package/template/templates/test-plan.md +0 -119
- package/template/templates/threat-model.md +0 -54
- package/template/templates/ui-brief.md +0 -49
- package/template/templates/verification.md +0 -63
- package/template/templates/vision.md +0 -34
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
"""Walk Claude Code transcript JSONL files for token economics and halt signals.
|
|
2
|
-
|
|
3
|
-
Generalized from scripts/audit-token-usage.py. Same cost-score weighting. Adds:
|
|
4
|
-
halt-sentinel detection, per-session start/end timestamps, cwd grouping for
|
|
5
|
-
Conductor worktree dedup, and (ADR-0009) per-phase usage summation bounded by a
|
|
6
|
-
sidecar's phase window.
|
|
7
|
-
|
|
8
|
-
Session→Skill attribution reads *structured invocation markers only* — the
|
|
9
|
-
`<command-name>` tag the harness writes for a slash command, and `Skill`
|
|
10
|
-
tool_use blocks. Free text is never matched: a prompt that merely mentions
|
|
11
|
-
`/hstack:coord` used to capture the whole session, which credited `coord` with
|
|
12
|
-
roughly half of all measured cache-read tokens (ADR-0009 § Context, defect 2).
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import re
|
|
19
|
-
from collections import defaultdict
|
|
20
|
-
from datetime import datetime, timezone
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from typing import Iterable
|
|
23
|
-
|
|
24
|
-
from telemetry.session_id import transcript_for_session
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
PROJECTS_ROOT = Path.home() / ".claude" / "projects"
|
|
28
|
-
|
|
29
|
-
# Structured markers only. `<command-name>` is emitted by the harness around a
|
|
30
|
-
# slash-command invocation; both the `/hstack:foo` and `/hstack-foo` spellings
|
|
31
|
-
# appear in real transcripts.
|
|
32
|
-
HSTACK_CMD_TAG = re.compile(r"<command-name>/?hstack[:\-]([a-z][a-z0-9\-]*)</command-name>")
|
|
33
|
-
# `Skill` tool_use input, optionally namespaced by a plugin prefix.
|
|
34
|
-
HSTACK_SKILL_NAME = re.compile(r"^(?:[A-Za-z0-9_.\-]+:)?hstack[:\-]([a-z][a-z0-9\-]*)$")
|
|
35
|
-
SUBAGENT_TYPE = re.compile(r'"subagent_type"\s*:\s*"([a-z][a-z0-9\-]*)"')
|
|
36
|
-
HALT_SENTINEL = re.compile(r"HSTACK-HALT:\s*reason=([a-z-]+)", re.IGNORECASE)
|
|
37
|
-
|
|
38
|
-
USAGE_KEYS = ("input_tokens", "cache_creation_input_tokens",
|
|
39
|
-
"cache_read_input_tokens", "output_tokens")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def message_text(msg) -> str:
|
|
43
|
-
if msg is None:
|
|
44
|
-
return ""
|
|
45
|
-
if isinstance(msg, str):
|
|
46
|
-
return msg
|
|
47
|
-
content = msg.get("content") if isinstance(msg, dict) else None
|
|
48
|
-
if content is None:
|
|
49
|
-
return ""
|
|
50
|
-
if isinstance(content, str):
|
|
51
|
-
return content
|
|
52
|
-
if isinstance(content, list):
|
|
53
|
-
parts = []
|
|
54
|
-
for blk in content:
|
|
55
|
-
if isinstance(blk, dict):
|
|
56
|
-
if blk.get("type") == "text" and isinstance(blk.get("text"), str):
|
|
57
|
-
parts.append(blk["text"])
|
|
58
|
-
else:
|
|
59
|
-
parts.append(json.dumps(blk, default=str))
|
|
60
|
-
else:
|
|
61
|
-
parts.append(str(blk))
|
|
62
|
-
return "\n".join(parts)
|
|
63
|
-
return json.dumps(content, default=str)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def discover_transcript_files(repo_paths: Iterable[Path]) -> list[Path]:
|
|
67
|
-
"""Discover transcript jsonl files for the given consuming-repo paths.
|
|
68
|
-
|
|
69
|
-
Each repo_path corresponds to a Claude Code project dir (`-Users-...` form).
|
|
70
|
-
We accept either real repo paths (we convert) or already-converted prefixes.
|
|
71
|
-
"""
|
|
72
|
-
if not PROJECTS_ROOT.exists():
|
|
73
|
-
return []
|
|
74
|
-
prefixes = []
|
|
75
|
-
for p in repo_paths:
|
|
76
|
-
p_str = str(Path(p).resolve()).replace("/", "-")
|
|
77
|
-
prefixes.append(p_str)
|
|
78
|
-
# Also match Conductor worktrees of the repo's basename
|
|
79
|
-
basename = Path(p).name
|
|
80
|
-
prefixes.append(f"-Users-hugoganet-conductor-workspaces-{basename}-")
|
|
81
|
-
files = []
|
|
82
|
-
for d in PROJECTS_ROOT.iterdir():
|
|
83
|
-
if not d.is_dir():
|
|
84
|
-
continue
|
|
85
|
-
name = d.name
|
|
86
|
-
if not any(name.startswith(prefix) or name == prefix.rstrip("-") for prefix in prefixes):
|
|
87
|
-
continue
|
|
88
|
-
for f in d.glob("*.jsonl"):
|
|
89
|
-
files.append(f)
|
|
90
|
-
return files
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def load_session(path: Path) -> list[dict]:
|
|
94
|
-
out = []
|
|
95
|
-
try:
|
|
96
|
-
with open(path, "r", errors="replace") as fh:
|
|
97
|
-
for line in fh:
|
|
98
|
-
line = line.strip()
|
|
99
|
-
if not line:
|
|
100
|
-
continue
|
|
101
|
-
try:
|
|
102
|
-
out.append(json.loads(line))
|
|
103
|
-
except json.JSONDecodeError:
|
|
104
|
-
continue
|
|
105
|
-
except OSError:
|
|
106
|
-
return []
|
|
107
|
-
return out
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def skill_tool_uses(msg) -> list[str]:
|
|
111
|
-
"""hstack Skill names invoked via a `Skill` tool_use block in this message.
|
|
112
|
-
|
|
113
|
-
Structural walk, not a regex over flattened text: the marker is the block's
|
|
114
|
-
`type`/`name`/`input.skill` triple, which prose cannot forge.
|
|
115
|
-
"""
|
|
116
|
-
if not isinstance(msg, dict):
|
|
117
|
-
return []
|
|
118
|
-
content = msg.get("content")
|
|
119
|
-
if not isinstance(content, list):
|
|
120
|
-
return []
|
|
121
|
-
out = []
|
|
122
|
-
for blk in content:
|
|
123
|
-
if not isinstance(blk, dict):
|
|
124
|
-
continue
|
|
125
|
-
if blk.get("type") != "tool_use" or blk.get("name") != "Skill":
|
|
126
|
-
continue
|
|
127
|
-
inp = blk.get("input")
|
|
128
|
-
name = inp.get("skill") if isinstance(inp, dict) else None
|
|
129
|
-
if not isinstance(name, str):
|
|
130
|
-
continue
|
|
131
|
-
m = HSTACK_SKILL_NAME.match(name.strip())
|
|
132
|
-
if m:
|
|
133
|
-
out.append(m.group(1))
|
|
134
|
-
return out
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def classify_session(records: list[dict]) -> tuple[str | None, set[str], list[str]]:
|
|
138
|
-
"""Return (skill_name, set of subagent types invoked, halt reasons).
|
|
139
|
-
|
|
140
|
-
`skill_name` is the first hstack Skill invoked through a structured marker —
|
|
141
|
-
a `<command-name>` tag or a `Skill` tool_use block. A session carrying
|
|
142
|
-
neither returns `None` and is reported as *unattributed* (ADR-0009 piece 3);
|
|
143
|
-
it is never captured by whatever hstack command its prompt happened to name.
|
|
144
|
-
"""
|
|
145
|
-
skill = None
|
|
146
|
-
subagents: set[str] = set()
|
|
147
|
-
halts: list[str] = []
|
|
148
|
-
for r in records:
|
|
149
|
-
t = r.get("type")
|
|
150
|
-
if t == "user":
|
|
151
|
-
if skill is None:
|
|
152
|
-
m = HSTACK_CMD_TAG.search(message_text(r.get("message")))
|
|
153
|
-
if m:
|
|
154
|
-
skill = m.group(1)
|
|
155
|
-
elif t == "assistant":
|
|
156
|
-
msg = r.get("message")
|
|
157
|
-
if skill is None:
|
|
158
|
-
invoked = skill_tool_uses(msg)
|
|
159
|
-
if invoked:
|
|
160
|
-
skill = invoked[0]
|
|
161
|
-
text = message_text(msg)
|
|
162
|
-
for sa in SUBAGENT_TYPE.findall(text):
|
|
163
|
-
subagents.add(sa)
|
|
164
|
-
for h in HALT_SENTINEL.findall(text):
|
|
165
|
-
halts.append(h.lower())
|
|
166
|
-
return skill, subagents, halts
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def parse_ts(value) -> datetime | None:
|
|
170
|
-
"""Parse an ISO-8601 stamp to an aware UTC datetime. None on anything else."""
|
|
171
|
-
if not isinstance(value, str) or not value:
|
|
172
|
-
return None
|
|
173
|
-
try:
|
|
174
|
-
dt = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
|
|
175
|
-
except ValueError:
|
|
176
|
-
return None
|
|
177
|
-
if dt.tzinfo is None:
|
|
178
|
-
dt = dt.replace(tzinfo=timezone.utc)
|
|
179
|
-
return dt.astimezone(timezone.utc)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def session_bounds(records: list[dict]) -> tuple[datetime | None, datetime | None]:
|
|
183
|
-
first = last = None
|
|
184
|
-
for r in records:
|
|
185
|
-
dt = parse_ts(r.get("timestamp"))
|
|
186
|
-
if dt is None:
|
|
187
|
-
continue
|
|
188
|
-
if first is None or dt < first:
|
|
189
|
-
first = dt
|
|
190
|
-
if last is None or dt > last:
|
|
191
|
-
last = dt
|
|
192
|
-
return first, last
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def tally_usage(records: list[dict], start: datetime | None = None,
|
|
196
|
-
end: datetime | None = None) -> dict[str, int]:
|
|
197
|
-
"""Sum assistant-turn usage. With `start`/`end`, only turns whose timestamp
|
|
198
|
-
falls inside the closed interval are counted."""
|
|
199
|
-
totals: dict[str, int] = defaultdict(int)
|
|
200
|
-
for k in USAGE_KEYS:
|
|
201
|
-
totals[k] = 0
|
|
202
|
-
totals["turns"] = 0
|
|
203
|
-
for r in records:
|
|
204
|
-
if r.get("type") != "assistant":
|
|
205
|
-
continue
|
|
206
|
-
if start is not None or end is not None:
|
|
207
|
-
ts = parse_ts(r.get("timestamp"))
|
|
208
|
-
if ts is None:
|
|
209
|
-
continue
|
|
210
|
-
if start is not None and ts < start:
|
|
211
|
-
continue
|
|
212
|
-
if end is not None and ts > end:
|
|
213
|
-
continue
|
|
214
|
-
usage = (r.get("message") or {}).get("usage") or {}
|
|
215
|
-
for k in USAGE_KEYS:
|
|
216
|
-
totals[k] += usage.get(k, 0) or 0
|
|
217
|
-
totals["turns"] += 1
|
|
218
|
-
return dict(totals)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def phase_usage(sidecar: dict, projects_root: Path | None = None) -> dict | None:
|
|
222
|
-
"""Token usage inside one phase window, per ADR-0009 piece 2.
|
|
223
|
-
|
|
224
|
-
Reads the transcript named by `sidecar["session_id"]` and sums assistant-turn
|
|
225
|
-
usage over records whose `timestamp` falls in
|
|
226
|
-
`[phase_opened_at, phase_closed_at]`.
|
|
227
|
-
|
|
228
|
-
Returns `None` — *unmeasured* — whenever the window cannot be honoured: a
|
|
229
|
-
schema_version-1 sidecar with no window, a null session id, a transcript
|
|
230
|
-
swept by `cleanupPeriodDays` or living on another machine, an unparseable or
|
|
231
|
-
inverted window. Never zero: zero is a measurement, and a phase whose
|
|
232
|
-
transcript is gone was not measured. Read-only; nothing is written.
|
|
233
|
-
"""
|
|
234
|
-
if not isinstance(sidecar, dict):
|
|
235
|
-
return None
|
|
236
|
-
opened = parse_ts(sidecar.get("phase_opened_at"))
|
|
237
|
-
closed = parse_ts(sidecar.get("phase_closed_at"))
|
|
238
|
-
if opened is None or closed is None or closed < opened:
|
|
239
|
-
return None
|
|
240
|
-
path = transcript_for_session(sidecar.get("session_id"), projects_root=projects_root)
|
|
241
|
-
if path is None:
|
|
242
|
-
return None
|
|
243
|
-
records = load_session(path)
|
|
244
|
-
if not records:
|
|
245
|
-
return None
|
|
246
|
-
totals = tally_usage(records, start=opened, end=closed)
|
|
247
|
-
total_tokens = sum(totals.get(k, 0) for k in USAGE_KEYS)
|
|
248
|
-
return {
|
|
249
|
-
**totals,
|
|
250
|
-
"total_tokens": total_tokens,
|
|
251
|
-
"cost_score": cost_score(totals),
|
|
252
|
-
"wall_clock_s": (closed - opened).total_seconds(),
|
|
253
|
-
"session_id": sidecar.get("session_id"),
|
|
254
|
-
"transcript": str(path),
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def cost_score(totals: dict[str, int]) -> int:
|
|
259
|
-
"""Same weighting as scripts/audit-token-usage.py: input + 1.25*cache_creation
|
|
260
|
-
+ 0.10*cache_read + 5*output. Approximates billable $-weight for ranking."""
|
|
261
|
-
return (
|
|
262
|
-
totals.get("input_tokens", 0)
|
|
263
|
-
+ int(totals.get("cache_creation_input_tokens", 0) * 1.25)
|
|
264
|
-
+ int(totals.get("cache_read_input_tokens", 0) * 0.10)
|
|
265
|
-
+ totals.get("output_tokens", 0) * 5
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def collect_session_rows(repo_paths: Iterable[Path], since: datetime | None = None) -> list[dict]:
|
|
270
|
-
"""Walk every transcript for the given repos and return one row per session."""
|
|
271
|
-
out = []
|
|
272
|
-
for f in discover_transcript_files(repo_paths):
|
|
273
|
-
recs = load_session(f)
|
|
274
|
-
if not recs:
|
|
275
|
-
continue
|
|
276
|
-
skill, subagents, halts = classify_session(recs)
|
|
277
|
-
first, last = session_bounds(recs)
|
|
278
|
-
if since is not None and last is not None and last < since:
|
|
279
|
-
continue
|
|
280
|
-
totals = tally_usage(recs)
|
|
281
|
-
if totals.get("turns", 0) == 0:
|
|
282
|
-
continue
|
|
283
|
-
out.append({
|
|
284
|
-
"file": f,
|
|
285
|
-
"project_dir": f.parent.name,
|
|
286
|
-
"skill": skill,
|
|
287
|
-
"subagents": sorted(subagents),
|
|
288
|
-
"halt_reasons": halts,
|
|
289
|
-
"started_at": first,
|
|
290
|
-
"ended_at": last,
|
|
291
|
-
"totals": totals,
|
|
292
|
-
"cost_score": cost_score(totals),
|
|
293
|
-
})
|
|
294
|
-
return out
|