blitz-cli 0.8.0__tar.gz → 0.10.0__tar.gz

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.
Files changed (59) hide show
  1. blitz_cli-0.10.0/.github/workflows/homebrew.yml +104 -0
  2. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/.gitignore +1 -1
  3. blitz_cli-0.10.0/.mcp.json +8 -0
  4. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/PKG-INFO +35 -13
  5. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/README.md +34 -12
  6. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/__init__.py +2 -2
  7. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_activity.py +79 -2
  8. blitz_cli-0.10.0/blitz_cli/_implement.py +412 -0
  9. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_ontology.py +149 -18
  10. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_playbooks.py +33 -9
  11. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_redact.py +1 -1
  12. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/cli.py +129 -53
  13. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/analyze.md +13 -13
  14. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/clarify.md +4 -4
  15. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/implement.md +28 -8
  16. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/plan.md +6 -6
  17. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/playbook.md +23 -23
  18. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/specify.md +6 -6
  19. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/tasks.md +79 -8
  20. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/scripts/bash/common.sh +31 -31
  21. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/scripts/bash/create-new-feature.sh +2 -2
  22. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/scripts/bash/setup-tasks.sh +1 -1
  23. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/templates/plan-template.md +3 -3
  24. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/workspace/huddle.md +8 -8
  25. blitz_cli-0.10.0/blitz_cli/templates/workspace_playbook.md.tmpl +32 -0
  26. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/pyproject.toml +1 -1
  27. blitz_cli-0.10.0/tests/test_implement.py +336 -0
  28. blitz_cli-0.10.0/tests/test_ontology.py +241 -0
  29. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_playbooks.py +29 -8
  30. blitz_cli-0.8.0/.github/workflows/homebrew.yml +0 -54
  31. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/.github/workflows/publish.yml +0 -0
  32. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/LICENSE +0 -0
  33. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_code_graph.py +0 -0
  34. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_corpus.py +0 -0
  35. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_dataset.py +0 -0
  36. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_git.py +0 -0
  37. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_make.py +0 -0
  38. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_scaffold.py +0 -0
  39. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_tool_schemas.py +0 -0
  40. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/_train.py +0 -0
  41. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/__init__.py +0 -0
  42. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/commands/__init__.py +0 -0
  43. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/memory/playbook.md +0 -0
  44. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/scripts/bash/check-prerequisites.sh +0 -0
  45. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/scripts/bash/setup-plan.sh +0 -0
  46. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/templates/spec-template.md +0 -0
  47. {blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks → blitz_cli-0.10.0/blitz_cli/playbooks/scaffold/.blitz}/templates/tasks-template.md +0 -0
  48. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/playbooks/workspace/__init__.py +0 -0
  49. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/templates/__init__.py +0 -0
  50. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/templates/locker_room_Makefile.tmpl +0 -0
  51. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/templates/locker_room_README.md.tmpl +0 -0
  52. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/blitz_cli/templates/locker_room_compose.yml.tmpl +0 -0
  53. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_activity.py +0 -0
  54. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_cli.py +0 -0
  55. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_corpus.py +0 -0
  56. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_make.py +0 -0
  57. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_redact.py +0 -0
  58. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_scaffold.py +0 -0
  59. {blitz_cli-0.8.0 → blitz_cli-0.10.0}/tests/test_tool_schemas.py +0 -0
@@ -0,0 +1,104 @@
1
+ name: Update Homebrew tap
2
+
3
+ # On every release tag, point the sparepartslabs/homebrew-tap formula at the new
4
+ # PyPI sdist. Runs alongside publish.yml (same v* trigger) and waits for the
5
+ # sdist to appear on PyPI before reading its canonical URL + sha256.
6
+ on:
7
+ push:
8
+ tags:
9
+ - "v*"
10
+
11
+ jobs:
12
+ bump:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Resolve version
16
+ id: v
17
+ run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
18
+
19
+ - name: Fetch sdist url + sha256 from PyPI (wait for publish)
20
+ id: pypi
21
+ run: |
22
+ VERSION="${{ steps.v.outputs.version }}"
23
+ for i in $(seq 1 30); do
24
+ JSON=$(curl -fsSL "https://pypi.org/pypi/blitz-cli/${VERSION}/json" || true)
25
+ URL=$(printf '%s' "$JSON" | jq -r '.urls[]? | select(.packagetype=="sdist") | .url')
26
+ SHA=$(printf '%s' "$JSON" | jq -r '.urls[]? | select(.packagetype=="sdist") | .digests.sha256')
27
+ if [ -n "$URL" ] && [ "$URL" != "null" ]; then break; fi
28
+ echo "sdist for ${VERSION} not on PyPI yet, retrying ($i)..."
29
+ sleep 10
30
+ done
31
+ if [ -z "$URL" ] || [ "$URL" = "null" ]; then
32
+ echo "::error::blitz-cli ${VERSION} sdist not found on PyPI"; exit 1
33
+ fi
34
+ echo "url=$URL" >> "$GITHUB_OUTPUT"
35
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
36
+
37
+ - name: Checkout tap
38
+ uses: actions/checkout@v4
39
+ with:
40
+ repository: sparepartslabs/homebrew-tap
41
+ token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
42
+
43
+ - name: Update formula (url, sha256, and vendored resources)
44
+ env:
45
+ VERSION: ${{ steps.v.outputs.version }}
46
+ URL: ${{ steps.pypi.outputs.url }}
47
+ SHA: ${{ steps.pypi.outputs.sha }}
48
+ run: |
49
+ F=Formula/blitz-cli.rb
50
+ sed -i -E "s#^ url \".*\"# url \"${URL}\"#" "$F"
51
+ sed -i -E "s#^ sha256 \".*\"# sha256 \"${SHA}\"#" "$F"
52
+
53
+ # Refresh the vendored `resource` blocks for blitz-cli's runtime deps
54
+ # (rich + its transitive deps). `brew update-python-resources` can't be
55
+ # used on release day (it skips PyPI uploads < 1 day old), so resolve
56
+ # the deps in a throwaway venv and read each sdist url+sha256 from PyPI.
57
+ python3 - "$F" "$VERSION" <<'PY'
58
+ import json, os, re, subprocess, sys, tempfile, urllib.request, venv
59
+
60
+ formula, version = sys.argv[1], sys.argv[2]
61
+ tmp = tempfile.mkdtemp()
62
+ venv.create(f"{tmp}/v", with_pip=True)
63
+ pip = f"{tmp}/v/bin/pip"
64
+ subprocess.check_call([pip, "install", "-q", f"blitz-cli=={version}"])
65
+ freeze = subprocess.check_output([pip, "list", "--format=freeze"]).decode()
66
+
67
+ skip = {"blitz-cli", "pip", "setuptools", "wheel"}
68
+ deps = []
69
+ for line in freeze.splitlines():
70
+ if "==" in line:
71
+ name, ver = line.split("==", 1)
72
+ if name.lower() not in skip:
73
+ deps.append((name, ver))
74
+
75
+ blocks = []
76
+ for name, ver in sorted(deps, key=lambda x: x[0].lower()):
77
+ d = json.load(urllib.request.urlopen(f"https://pypi.org/pypi/{name}/{ver}/json"))
78
+ sd = next((u for u in d["urls"] if u["packagetype"] == "sdist"), None)
79
+ if not sd:
80
+ sys.exit(f"no sdist for {name} {ver}")
81
+ blocks.append(
82
+ f' resource "{d["info"]["name"]}" do\n'
83
+ f' url "{sd["url"]}"\n'
84
+ f' sha256 "{sd["digests"]["sha256"]}"\n'
85
+ f' end'
86
+ )
87
+
88
+ text = open(formula).read()
89
+ # drop existing resource blocks, then insert fresh ones after depends_on
90
+ text = re.sub(r'\n resource "[^"]+" do\n(?:.*\n)*? end\n', '\n', text)
91
+ inject = "\n" + "\n\n".join(blocks) + "\n"
92
+ text = re.sub(r'( depends_on "python@[^"]+"\n)', r'\1' + inject, text, count=1)
93
+ text = re.sub(r'\n{3,}', '\n\n', text) # collapse blank runs (idempotent)
94
+ open(formula, "w").write(text)
95
+ print(f"refreshed {len(blocks)} resources: {', '.join(n for n, _ in deps)}")
96
+ PY
97
+
98
+ git config user.name "github-actions[bot]"
99
+ git config user.email "github-actions[bot]@users.noreply.github.com"
100
+ if git diff --quiet; then
101
+ echo "formula already up to date"; exit 0
102
+ fi
103
+ git commit -am "blitz-cli ${VERSION}"
104
+ git push
@@ -8,4 +8,4 @@ dist/
8
8
 
9
9
  # stray test artifacts from running the CLI in-repo
10
10
  /.claude/
11
- /.playbooks/
11
+ /.blitz/
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "locker-room": {
4
+ "type": "sse",
5
+ "url": "http://localhost:8848/sse"
6
+ }
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blitz-cli
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Developer CLI for Blitz locker-room: spec-driven playbooks, a codebase/huddle ontology graph with an MCP tool, and the composable watcher/trainer/graph containers.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -31,18 +31,18 @@ The core CLI is stdlib plus `rich`. Two optional extras carry heavier deps only
31
31
  when you need them: `blitz-cli[locker-room]` (the MCP server) and
32
32
  `blitz-cli[train]` (the QLoRA trainer).
33
33
 
34
- Top-level commands: `playbooks`, `locker-room`, `activity`.
34
+ Top-level commands: `init`, `implement`, `locker-room`.
35
35
 
36
36
  ## Playbooks: spec-driven development commands
37
37
 
38
- `blitz playbooks init` installs a set of spec-driven-development slash-commands
38
+ `blitz init` installs a set of spec-driven-development slash-commands
39
39
  into a repo, rendered for whichever agent you use (Claude Code, Cursor, GitHub
40
- Copilot, Gemini CLI, OpenCode), plus a shared `.playbooks/` working area.
40
+ Copilot, Gemini CLI, OpenCode), plus a shared `.blitz/` working area.
41
41
 
42
42
  ```bash
43
- blitz playbooks init # auto-detect the agent(s) in this repo
44
- blitz playbooks init --agent claude # or target one explicitly
45
- blitz playbooks init --all # install for every supported agent
43
+ blitz init # auto-detect the agent(s) in this repo
44
+ blitz init --agent claude # or target one explicitly
45
+ blitz init --all # install for every supported agent
46
46
  ```
47
47
 
48
48
  The commands are `specify`, `plan`, `tasks`, `implement`, `playbook`, `clarify`,
@@ -51,6 +51,28 @@ and `analyze`. Run at a folder of repos, `init` installs into each repo it finds
51
51
  for coordinating a feature across repos. Use `--ignore PATH` to skip a repo and
52
52
  `--root` to force a single install.
53
53
 
54
+ ## Implement: execute a tasks.md with a local model
55
+
56
+ `blitz implement` drives a local model (served by Ollama) through a
57
+ playbook-generated `tasks.md`, one task at a time. The model only proposes
58
+ actions (full-file writes and shell commands); the driver applies them, runs
59
+ each phase's machine-runnable verification, retries with the failure fed back
60
+ (default 2), snapshots every file before overwriting it, and checks off
61
+ `- [x]` in the tasks.md. A task that keeps failing stops the run so the state
62
+ stays inspectable. Per-attempt model replies, snapshots, and exact token
63
+ accounting (`usage.json`, per task and total) land under
64
+ `.blitz/implement/runs/<timestamp>/`.
65
+
66
+ ```bash
67
+ blitz implement # auto-detect the open specs/*/tasks.md
68
+ blitz implement specs/001-my-feature/tasks.md # or name it
69
+ blitz implement --model qwen2.5-14b-28k --start-at T005
70
+ ```
71
+
72
+ Tasks whose text says "optional manual" are skipped and left unchecked. The
73
+ Ollama endpoint comes from `--ollama-url` or `OLLAMA_HOST`; the model from
74
+ `--model` or `BLITZ_IMPLEMENT_MODEL`.
75
+
54
76
  ## Locker-room: ontology graph, watcher, trainer, MCP
55
77
 
56
78
  The locker-room is a **context node**: a set of containers that ingest, build,
@@ -98,17 +120,17 @@ the base model with `BASE_MODEL_HF` or `blitz locker-room init --base-model`.
98
120
 
99
121
  ## Activity: replay a coding-agent session
100
122
 
101
- `blitz activity` reads your coding agent's own local session transcripts
123
+ `blitz locker-room activity` reads your coding agent's own local session transcripts
102
124
  (currently Claude Code's, at `~/.claude/projects`) for the current repo and
103
125
  replays each prompt alongside the actions taken to resolve it (tool calls,
104
126
  sub-agents) and the final response:
105
127
 
106
128
  ```bash
107
- blitz activity --list # sessions for this repo, most recent first
108
- blitz activity # the most recent session's timeline
109
- blitz activity <session-id-prefix> # a specific session
110
- blitz activity --full # untruncated prompts/responses/tool I/O
111
- blitz activity --html report.html # also write a self-contained HTML report
129
+ blitz locker-room activity --list # sessions for this repo, most recent first
130
+ blitz locker-room activity # the most recent session's timeline
131
+ blitz locker-room activity <session-id-prefix> # a specific session
132
+ blitz locker-room activity --full # untruncated prompts/responses/tool I/O
133
+ blitz locker-room activity --html report.html # also write a self-contained HTML report
112
134
  ```
113
135
 
114
136
  The terminal view is compact by default; `--full` shows untruncated text. The
@@ -12,18 +12,18 @@ The core CLI is stdlib plus `rich`. Two optional extras carry heavier deps only
12
12
  when you need them: `blitz-cli[locker-room]` (the MCP server) and
13
13
  `blitz-cli[train]` (the QLoRA trainer).
14
14
 
15
- Top-level commands: `playbooks`, `locker-room`, `activity`.
15
+ Top-level commands: `init`, `implement`, `locker-room`.
16
16
 
17
17
  ## Playbooks: spec-driven development commands
18
18
 
19
- `blitz playbooks init` installs a set of spec-driven-development slash-commands
19
+ `blitz init` installs a set of spec-driven-development slash-commands
20
20
  into a repo, rendered for whichever agent you use (Claude Code, Cursor, GitHub
21
- Copilot, Gemini CLI, OpenCode), plus a shared `.playbooks/` working area.
21
+ Copilot, Gemini CLI, OpenCode), plus a shared `.blitz/` working area.
22
22
 
23
23
  ```bash
24
- blitz playbooks init # auto-detect the agent(s) in this repo
25
- blitz playbooks init --agent claude # or target one explicitly
26
- blitz playbooks init --all # install for every supported agent
24
+ blitz init # auto-detect the agent(s) in this repo
25
+ blitz init --agent claude # or target one explicitly
26
+ blitz init --all # install for every supported agent
27
27
  ```
28
28
 
29
29
  The commands are `specify`, `plan`, `tasks`, `implement`, `playbook`, `clarify`,
@@ -32,6 +32,28 @@ and `analyze`. Run at a folder of repos, `init` installs into each repo it finds
32
32
  for coordinating a feature across repos. Use `--ignore PATH` to skip a repo and
33
33
  `--root` to force a single install.
34
34
 
35
+ ## Implement: execute a tasks.md with a local model
36
+
37
+ `blitz implement` drives a local model (served by Ollama) through a
38
+ playbook-generated `tasks.md`, one task at a time. The model only proposes
39
+ actions (full-file writes and shell commands); the driver applies them, runs
40
+ each phase's machine-runnable verification, retries with the failure fed back
41
+ (default 2), snapshots every file before overwriting it, and checks off
42
+ `- [x]` in the tasks.md. A task that keeps failing stops the run so the state
43
+ stays inspectable. Per-attempt model replies, snapshots, and exact token
44
+ accounting (`usage.json`, per task and total) land under
45
+ `.blitz/implement/runs/<timestamp>/`.
46
+
47
+ ```bash
48
+ blitz implement # auto-detect the open specs/*/tasks.md
49
+ blitz implement specs/001-my-feature/tasks.md # or name it
50
+ blitz implement --model qwen2.5-14b-28k --start-at T005
51
+ ```
52
+
53
+ Tasks whose text says "optional manual" are skipped and left unchecked. The
54
+ Ollama endpoint comes from `--ollama-url` or `OLLAMA_HOST`; the model from
55
+ `--model` or `BLITZ_IMPLEMENT_MODEL`.
56
+
35
57
  ## Locker-room: ontology graph, watcher, trainer, MCP
36
58
 
37
59
  The locker-room is a **context node**: a set of containers that ingest, build,
@@ -79,17 +101,17 @@ the base model with `BASE_MODEL_HF` or `blitz locker-room init --base-model`.
79
101
 
80
102
  ## Activity: replay a coding-agent session
81
103
 
82
- `blitz activity` reads your coding agent's own local session transcripts
104
+ `blitz locker-room activity` reads your coding agent's own local session transcripts
83
105
  (currently Claude Code's, at `~/.claude/projects`) for the current repo and
84
106
  replays each prompt alongside the actions taken to resolve it (tool calls,
85
107
  sub-agents) and the final response:
86
108
 
87
109
  ```bash
88
- blitz activity --list # sessions for this repo, most recent first
89
- blitz activity # the most recent session's timeline
90
- blitz activity <session-id-prefix> # a specific session
91
- blitz activity --full # untruncated prompts/responses/tool I/O
92
- blitz activity --html report.html # also write a self-contained HTML report
110
+ blitz locker-room activity --list # sessions for this repo, most recent first
111
+ blitz locker-room activity # the most recent session's timeline
112
+ blitz locker-room activity <session-id-prefix> # a specific session
113
+ blitz locker-room activity --full # untruncated prompts/responses/tool I/O
114
+ blitz locker-room activity --html report.html # also write a self-contained HTML report
93
115
  ```
94
116
 
95
117
  The terminal view is compact by default; `--full` shows untruncated text. The
@@ -13,7 +13,7 @@ Commands:
13
13
  - ``blitz traces list`` / ``blitz traces show <trace_id>`` — inspect captured
14
14
  traces from the local SQLite store.
15
15
 
16
- - ``blitz activity [session] [--list] [--html report.html]`` — replay a
16
+ - ``blitz locker-room activity [session] [--list] [--html report.html]`` — replay a
17
17
  Claude Code session for this repo: each prompt you sent, the actions taken
18
18
  to resolve it (tool calls, sub-agents), and the final response. Reads
19
19
  ``~/.claude/projects`` directly; unrelated to Blitz's own captured traces.
@@ -30,4 +30,4 @@ Commands:
30
30
 
31
31
  __all__ = ["__version__"]
32
32
 
33
- __version__ = "0.6.0"
33
+ __version__ = "0.10.0"
@@ -1,4 +1,4 @@
1
- """`blitz activity` — replay a coding-agent session: what you asked, and how it
1
+ """`blitz locker-room activity` — replay a coding-agent session: what you asked, and how it
2
2
  was resolved.
3
3
 
4
4
  The currently-supported transcript format is Claude Code's, at
@@ -82,6 +82,83 @@ def default_sessions_dir() -> Path:
82
82
  return Path.home() / ".claude" / "projects"
83
83
 
84
84
 
85
+ def _zero_usage(**extra) -> dict:
86
+ return {"input": 0, "output": 0, "cache_read": 0, "cache_creation": 0, **extra}
87
+
88
+
89
+ def _add_usage(agg: dict, usage: dict) -> None:
90
+ agg["input"] += usage.get("input_tokens") or 0
91
+ agg["output"] += usage.get("output_tokens") or 0
92
+ agg["cache_read"] += usage.get("cache_read_input_tokens") or 0
93
+ agg["cache_creation"] += usage.get("cache_creation_input_tokens") or 0
94
+
95
+
96
+ def frontier_usage_by_slug(sessions_root: Path, slugs) -> dict:
97
+ """Attribute frontier-agent (Claude Code) token spend to feature specs by
98
+ scanning session transcripts under ``sessions_root`` for ``specs/<slug>``
99
+ mentions, at two granularities:
100
+
101
+ - ``session``: the whole session's usage goes to the single most-mentioned
102
+ slug (sessions mentioning no spec are ignored). An upper bound — long
103
+ sessions that merely discuss a spec inflate it.
104
+ - ``turn``: each assistant message is attributed to the slug most recently
105
+ mentioned at that point in the transcript, so usage before the spec
106
+ comes up — or after the session moves on to another spec — is excluded.
107
+ The closer proxy for implementation cost.
108
+
109
+ Usage is deduplicated by API message id (a transcript writes several
110
+ entries per message, each repeating the same usage block). Returns
111
+ ``{slug: {"session": {input, output, cache_read, cache_creation, sessions},
112
+ "turn": {input, output, cache_read, cache_creation, messages}}}``."""
113
+ needles = {slug: f"specs/{slug}" for slug in slugs}
114
+ totals: dict = {}
115
+
116
+ def slot(slug: str) -> dict:
117
+ return totals.setdefault(slug, {
118
+ "session": _zero_usage(sessions=0),
119
+ "turn": _zero_usage(messages=0),
120
+ })
121
+
122
+ for jsonl in sorted(sessions_root.glob("*/*.jsonl")):
123
+ try:
124
+ text = jsonl.read_text(encoding="utf-8", errors="replace")
125
+ except OSError:
126
+ continue
127
+ counts = {slug: text.count(n) for slug, n in needles.items()}
128
+ best = max(counts, key=counts.get) if counts else None
129
+ if not best or counts[best] == 0:
130
+ continue
131
+ current: str | None = None # sticky most-recently-mentioned slug
132
+ by_message: dict = {} # message id -> (usage, slug at that point)
133
+ for i, line in enumerate(text.splitlines()):
134
+ mentioned = [(line.find(n), slug) for slug, n in needles.items() if n in line]
135
+ if mentioned:
136
+ current = max(mentioned)[1] # last mention in the line wins
137
+ if '"usage"' not in line:
138
+ continue
139
+ try:
140
+ rec = json.loads(line)
141
+ except ValueError:
142
+ continue
143
+ if rec.get("type") != "assistant":
144
+ continue
145
+ message = rec.get("message", {})
146
+ usage = message.get("usage")
147
+ if usage:
148
+ by_message[message.get("id") or f"line:{i}"] = (usage, current)
149
+ if not by_message:
150
+ continue
151
+ session_agg = slot(best)["session"]
152
+ for usage, slug in by_message.values():
153
+ _add_usage(session_agg, usage)
154
+ if slug is not None:
155
+ turn_agg = slot(slug)["turn"]
156
+ _add_usage(turn_agg, usage)
157
+ turn_agg["messages"] += 1
158
+ session_agg["sessions"] += 1
159
+ return totals
160
+
161
+
85
162
  def default_file_history_dir() -> Path:
86
163
  """Root of Claude Code's own pre-edit file backups, one subdir per
87
164
  session id — a separate store from ~/.claude/projects, linked to it via
@@ -657,7 +734,7 @@ def render_html(session: Session, limit: int) -> str:
657
734
  '<html lang="en">',
658
735
  "<head>",
659
736
  '<meta charset="utf-8">',
660
- f"<title>blitz activity — {html.escape(session.title)}</title>",
737
+ f"<title>blitz locker-room activity — {html.escape(session.title)}</title>",
661
738
  f"<style>{_HTML_STYLE}</style>",
662
739
  "</head>",
663
740
  "<body>",