blitz-cli 0.5.0__tar.gz → 0.7.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.
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/.github/workflows/publish.yml +5 -0
- blitz_cli-0.7.0/.gitignore +11 -0
- blitz_cli-0.7.0/PKG-INFO +118 -0
- blitz_cli-0.7.0/README.md +97 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/blitz_cli/__init__.py +1 -1
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/blitz_cli/_activity.py +12 -10
- blitz_cli-0.7.0/blitz_cli/_code_graph.py +278 -0
- blitz_cli-0.5.0/blitz_cli/_codeassist.py → blitz_cli-0.7.0/blitz_cli/_corpus.py +3 -3
- blitz_cli-0.7.0/blitz_cli/_git.py +120 -0
- blitz_cli-0.7.0/blitz_cli/_locker_room_mcp.py +117 -0
- blitz_cli-0.7.0/blitz_cli/_ontology.py +417 -0
- blitz_cli-0.7.0/blitz_cli/_playbooks.py +267 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/blitz_cli/_redact.py +2 -2
- blitz_cli-0.7.0/blitz_cli/_scaffold.py +137 -0
- blitz_cli-0.7.0/blitz_cli/_tool_schemas.py +189 -0
- blitz_cli-0.7.0/blitz_cli/_train.py +247 -0
- blitz_cli-0.7.0/blitz_cli/cli.py +525 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/__init__.py +0 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/analyze.md +255 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/clarify.md +285 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/implement.md +219 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/plan.md +169 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/playbook.md +152 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/specify.md +345 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/commands/tasks.md +218 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/memory/playbook.md +50 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/check-prerequisites.sh +195 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/common.sh +692 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/create-new-feature.sh +301 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/setup-plan.sh +83 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/setup-tasks.sh +91 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/templates/plan-template.md +113 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/templates/spec-template.md +131 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/templates/tasks-template.md +252 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/workspace/__init__.py +0 -0
- blitz_cli-0.7.0/blitz_cli/playbooks/workspace/huddle.md +101 -0
- blitz_cli-0.7.0/blitz_cli/templates/locker_room_Makefile.tmpl +40 -0
- blitz_cli-0.7.0/blitz_cli/templates/locker_room_README.md.tmpl +39 -0
- blitz_cli-0.7.0/blitz_cli/templates/locker_room_compose.yml.tmpl +66 -0
- blitz_cli-0.7.0/pyproject.toml +49 -0
- blitz_cli-0.7.0/tests/test_cli.py +66 -0
- blitz_cli-0.5.0/tests/test_codeassist.py → blitz_cli-0.7.0/tests/test_corpus.py +21 -21
- blitz_cli-0.7.0/tests/test_playbooks.py +98 -0
- blitz_cli-0.7.0/tests/test_scaffold.py +155 -0
- blitz_cli-0.7.0/tests/test_tool_schemas.py +117 -0
- blitz_cli-0.5.0/.gitignore +0 -7
- blitz_cli-0.5.0/PKG-INFO +0 -95
- blitz_cli-0.5.0/README.md +0 -84
- blitz_cli-0.5.0/blitz_cli/_client.py +0 -197
- blitz_cli-0.5.0/blitz_cli/_local.py +0 -251
- blitz_cli-0.5.0/blitz_cli/_release.py +0 -396
- blitz_cli-0.5.0/blitz_cli/_scaffold.py +0 -167
- blitz_cli-0.5.0/blitz_cli/_scan/__init__.py +0 -71
- blitz_cli-0.5.0/blitz_cli/_scan/_classify.py +0 -28
- blitz_cli-0.5.0/blitz_cli/_scan/_js.py +0 -101
- blitz_cli-0.5.0/blitz_cli/_scan/_models.py +0 -132
- blitz_cli-0.5.0/blitz_cli/_scan/_python.py +0 -311
- blitz_cli-0.5.0/blitz_cli/_scan/_report.py +0 -205
- blitz_cli-0.5.0/blitz_cli/_scan/_walk.py +0 -68
- blitz_cli-0.5.0/blitz_cli/_skill.py +0 -59
- blitz_cli-0.5.0/blitz_cli/_sync.py +0 -129
- blitz_cli-0.5.0/blitz_cli/_toml.py +0 -63
- blitz_cli-0.5.0/blitz_cli/_traces.py +0 -194
- blitz_cli-0.5.0/blitz_cli/cli.py +0 -862
- blitz_cli-0.5.0/blitz_cli/skills/blitz-fix-teacher/SKILL.md +0 -180
- blitz_cli-0.5.0/blitz_cli/skills/blitz-instrument/SKILL.md +0 -167
- blitz_cli-0.5.0/blitz_cli/skills/blitz-setup-training/SKILL.md +0 -164
- blitz_cli-0.5.0/blitz_cli/skills/blitz-simulate-traces/SKILL.md +0 -148
- blitz_cli-0.5.0/blitz_cli/templates/Dockerfile.tmpl +0 -23
- blitz_cli-0.5.0/blitz_cli/templates/Makefile.tmpl +0 -79
- blitz_cli-0.5.0/blitz_cli/templates/README.md.tmpl +0 -84
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_Dockerfile.tmpl +0 -21
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +0 -17
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_Makefile.tmpl +0 -74
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_README.md.tmpl +0 -91
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_dockerignore.tmpl +0 -9
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_requirements.txt.tmpl +0 -4
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +0 -15
- blitz_cli-0.5.0/blitz_cli/templates/codeassist_train.py.tmpl +0 -187
- blitz_cli-0.5.0/blitz_cli/templates/dockerignore.tmpl +0 -9
- blitz_cli-0.5.0/blitz_cli/templates/requirements.txt.tmpl +0 -16
- blitz_cli-0.5.0/blitz_cli/templates/serve.py.tmpl +0 -125
- blitz_cli-0.5.0/blitz_cli/templates/train.py.tmpl +0 -182
- blitz_cli-0.5.0/blitz_cli/templates/watch.py.tmpl +0 -46
- blitz_cli-0.5.0/pyproject.toml +0 -33
- blitz_cli-0.5.0/tests/test_cli.py +0 -79
- blitz_cli-0.5.0/tests/test_local.py +0 -350
- blitz_cli-0.5.0/tests/test_release.py +0 -315
- blitz_cli-0.5.0/tests/test_scaffold.py +0 -274
- blitz_cli-0.5.0/tests/test_scan.py +0 -308
- blitz_cli-0.5.0/tests/test_serve.py +0 -140
- blitz_cli-0.5.0/tests/test_sync.py +0 -72
- blitz_cli-0.5.0/tests/test_toml.py +0 -122
- blitz_cli-0.5.0/tests/test_traces.py +0 -98
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/.github/workflows/homebrew.yml +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/LICENSE +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/blitz_cli/_dataset.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/blitz_cli/_make.py +0 -0
- {blitz_cli-0.5.0/blitz_cli/skills → blitz_cli-0.7.0/blitz_cli/playbooks}/__init__.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/blitz_cli/templates/__init__.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/tests/test_activity.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/tests/test_make.py +0 -0
- {blitz_cli-0.5.0 → blitz_cli-0.7.0}/tests/test_redact.py +0 -0
|
@@ -28,3 +28,8 @@ jobs:
|
|
|
28
28
|
|
|
29
29
|
- name: Publish to PyPI
|
|
30
30
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
31
|
+
|
|
32
|
+
# The locker-room images are published from the sparepartslabs/blitz-locker-room
|
|
33
|
+
# repo, which declares the blitz-cli versions it supports and builds them itself
|
|
34
|
+
# (no cross-repo trigger or token here). After releasing a version blitz-cli,
|
|
35
|
+
# add it to that repo's supported-versions.txt to publish its images.
|
blitz_cli-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blitz-cli
|
|
3
|
+
Version: 0.7.0
|
|
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
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: rich>=13
|
|
9
|
+
Provides-Extra: locker-room
|
|
10
|
+
Requires-Dist: mcp>=1.0; extra == 'locker-room'
|
|
11
|
+
Provides-Extra: train
|
|
12
|
+
Requires-Dist: accelerate==1.2.1; extra == 'train'
|
|
13
|
+
Requires-Dist: datasets==3.2.0; extra == 'train'
|
|
14
|
+
Requires-Dist: liger-kernel==0.5.2; extra == 'train'
|
|
15
|
+
Requires-Dist: mlflow==2.19.0; extra == 'train'
|
|
16
|
+
Requires-Dist: peft==0.14.0; extra == 'train'
|
|
17
|
+
Requires-Dist: sentencepiece==0.2.0; extra == 'train'
|
|
18
|
+
Requires-Dist: transformers==4.48.0; extra == 'train'
|
|
19
|
+
Requires-Dist: trl==0.13.0; extra == 'train'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# blitz-cli
|
|
23
|
+
|
|
24
|
+
Developer CLI for the Blitz **locker-room**: spec-driven playbooks for your
|
|
25
|
+
coding agent, a local ontology graph of your codebase/huddles/features (served to
|
|
26
|
+
agents over MCP), and the composable containers that keep it fresh.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install blitz-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The core CLI is stdlib plus `rich`. Two optional extras carry heavier deps only
|
|
33
|
+
when you need them: `blitz-cli[locker-room]` (the MCP server) and
|
|
34
|
+
`blitz-cli[train]` (the QLoRA trainer).
|
|
35
|
+
|
|
36
|
+
Top-level commands: `playbooks`, `locker-room`, `activity`.
|
|
37
|
+
|
|
38
|
+
## Playbooks: spec-driven development commands
|
|
39
|
+
|
|
40
|
+
`blitz playbooks init` installs a set of spec-driven-development slash-commands
|
|
41
|
+
into a repo, rendered for whichever agent you use (Claude Code, Cursor, GitHub
|
|
42
|
+
Copilot, Gemini CLI, OpenCode), plus a shared `.playbooks/` working area.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
blitz playbooks init # auto-detect the agent(s) in this repo
|
|
46
|
+
blitz playbooks init --agent claude # or target one explicitly
|
|
47
|
+
blitz playbooks init --all # install for every supported agent
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The commands are `specify`, `plan`, `tasks`, `implement`, `playbook`, `clarify`,
|
|
51
|
+
and `analyze`. Run at a folder of repos, `init` installs into each repo it finds
|
|
52
|
+
(recursing through non-git folders) and adds a workspace-level `/huddle` command
|
|
53
|
+
for coordinating a feature across repos. Use `--ignore PATH` to skip a repo and
|
|
54
|
+
`--root` to force a single install.
|
|
55
|
+
|
|
56
|
+
## Locker-room: ontology graph, watcher, trainer, MCP
|
|
57
|
+
|
|
58
|
+
`blitz locker-room init` scaffolds a `docker-compose.yml` that pulls prebuilt
|
|
59
|
+
images (from `sparepartslabs/blitz-locker-room`, pinned to this CLI version) for
|
|
60
|
+
three roles sharing `./data`:
|
|
61
|
+
|
|
62
|
+
- **watch**: ingests your coding agent's session transcripts into a corpus.
|
|
63
|
+
- **graph**: rebuilds an ontology of your playbook-enabled repos, huddles,
|
|
64
|
+
features, and their code lineage into `data/ontology.db` on a loop.
|
|
65
|
+
- **mcp**: serves that ontology to agents over SSE.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
blitz locker-room init # write ./locker-room/docker-compose.yml
|
|
69
|
+
cd locker-room
|
|
70
|
+
blitz locker-room up # start watch + graph + mcp
|
|
71
|
+
blitz locker-room logs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The ontology is also usable directly on the host, no containers required:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
blitz locker-room ontologize # scan a workspace into ./data/ontology.db
|
|
78
|
+
blitz locker-room list # list playbook-enabled repos + their active feature
|
|
79
|
+
blitz locker-room mcp # stdio MCP server for a local agent
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### The graph an agent can query
|
|
83
|
+
|
|
84
|
+
`ontologize` reuses playbook discovery and static code parsing (Python symbols +
|
|
85
|
+
imports + calls; File nodes for other languages) to build a graph of `Workspace`,
|
|
86
|
+
`Repo`, `AgentTool`, `Playbook`, `Feature` (with lifecycle stage), `Huddle`,
|
|
87
|
+
`File`, and `Symbol` nodes. Each feature's git branch diff links it to the code
|
|
88
|
+
it touches (`Feature -TOUCHES-> File/Symbol`), so an agent can walk a spec out to
|
|
89
|
+
the full lineage of code it implements, and back. The MCP tools expose this:
|
|
90
|
+
`list_repos`, `get_repo`, `list_huddles`, `get_huddle`, `get_playbooks`,
|
|
91
|
+
`get_lineage`, `impact_of`, `get_file`, `get_symbol`, and a generic `neighbors`.
|
|
92
|
+
|
|
93
|
+
### Training (optional)
|
|
94
|
+
|
|
95
|
+
`blitz locker-room train` runs an on-demand QLoRA fine-tune of the watcher corpus
|
|
96
|
+
in the trainer container (needs a GPU); adapters land in `./train-out`. Override
|
|
97
|
+
the base model with `BASE_MODEL_HF` or `blitz locker-room init --base-model`.
|
|
98
|
+
|
|
99
|
+
## Activity: replay a coding-agent session
|
|
100
|
+
|
|
101
|
+
`blitz activity` reads your coding agent's own local session transcripts
|
|
102
|
+
(currently Claude Code's, at `~/.claude/projects`) for the current repo and
|
|
103
|
+
replays each prompt alongside the actions taken to resolve it (tool calls,
|
|
104
|
+
sub-agents) and the final response:
|
|
105
|
+
|
|
106
|
+
```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
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The terminal view is compact by default; `--full` shows untruncated text. The
|
|
115
|
+
HTML report includes each action's full tool input/output, collapsed per action.
|
|
116
|
+
Each turn shows its raw token usage and, where a pre-edit backup exists, a real
|
|
117
|
+
diff of what an `Edit`/`Write` changed. Point at a different transcript root with
|
|
118
|
+
`--sessions-dir`. This only reads local session data, no project id or API key.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# blitz-cli
|
|
2
|
+
|
|
3
|
+
Developer CLI for the Blitz **locker-room**: spec-driven playbooks for your
|
|
4
|
+
coding agent, a local ontology graph of your codebase/huddles/features (served to
|
|
5
|
+
agents over MCP), and the composable containers that keep it fresh.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install blitz-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The core CLI is stdlib plus `rich`. Two optional extras carry heavier deps only
|
|
12
|
+
when you need them: `blitz-cli[locker-room]` (the MCP server) and
|
|
13
|
+
`blitz-cli[train]` (the QLoRA trainer).
|
|
14
|
+
|
|
15
|
+
Top-level commands: `playbooks`, `locker-room`, `activity`.
|
|
16
|
+
|
|
17
|
+
## Playbooks: spec-driven development commands
|
|
18
|
+
|
|
19
|
+
`blitz playbooks init` installs a set of spec-driven-development slash-commands
|
|
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.
|
|
22
|
+
|
|
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
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The commands are `specify`, `plan`, `tasks`, `implement`, `playbook`, `clarify`,
|
|
30
|
+
and `analyze`. Run at a folder of repos, `init` installs into each repo it finds
|
|
31
|
+
(recursing through non-git folders) and adds a workspace-level `/huddle` command
|
|
32
|
+
for coordinating a feature across repos. Use `--ignore PATH` to skip a repo and
|
|
33
|
+
`--root` to force a single install.
|
|
34
|
+
|
|
35
|
+
## Locker-room: ontology graph, watcher, trainer, MCP
|
|
36
|
+
|
|
37
|
+
`blitz locker-room init` scaffolds a `docker-compose.yml` that pulls prebuilt
|
|
38
|
+
images (from `sparepartslabs/blitz-locker-room`, pinned to this CLI version) for
|
|
39
|
+
three roles sharing `./data`:
|
|
40
|
+
|
|
41
|
+
- **watch**: ingests your coding agent's session transcripts into a corpus.
|
|
42
|
+
- **graph**: rebuilds an ontology of your playbook-enabled repos, huddles,
|
|
43
|
+
features, and their code lineage into `data/ontology.db` on a loop.
|
|
44
|
+
- **mcp**: serves that ontology to agents over SSE.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
blitz locker-room init # write ./locker-room/docker-compose.yml
|
|
48
|
+
cd locker-room
|
|
49
|
+
blitz locker-room up # start watch + graph + mcp
|
|
50
|
+
blitz locker-room logs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The ontology is also usable directly on the host, no containers required:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
blitz locker-room ontologize # scan a workspace into ./data/ontology.db
|
|
57
|
+
blitz locker-room list # list playbook-enabled repos + their active feature
|
|
58
|
+
blitz locker-room mcp # stdio MCP server for a local agent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### The graph an agent can query
|
|
62
|
+
|
|
63
|
+
`ontologize` reuses playbook discovery and static code parsing (Python symbols +
|
|
64
|
+
imports + calls; File nodes for other languages) to build a graph of `Workspace`,
|
|
65
|
+
`Repo`, `AgentTool`, `Playbook`, `Feature` (with lifecycle stage), `Huddle`,
|
|
66
|
+
`File`, and `Symbol` nodes. Each feature's git branch diff links it to the code
|
|
67
|
+
it touches (`Feature -TOUCHES-> File/Symbol`), so an agent can walk a spec out to
|
|
68
|
+
the full lineage of code it implements, and back. The MCP tools expose this:
|
|
69
|
+
`list_repos`, `get_repo`, `list_huddles`, `get_huddle`, `get_playbooks`,
|
|
70
|
+
`get_lineage`, `impact_of`, `get_file`, `get_symbol`, and a generic `neighbors`.
|
|
71
|
+
|
|
72
|
+
### Training (optional)
|
|
73
|
+
|
|
74
|
+
`blitz locker-room train` runs an on-demand QLoRA fine-tune of the watcher corpus
|
|
75
|
+
in the trainer container (needs a GPU); adapters land in `./train-out`. Override
|
|
76
|
+
the base model with `BASE_MODEL_HF` or `blitz locker-room init --base-model`.
|
|
77
|
+
|
|
78
|
+
## Activity: replay a coding-agent session
|
|
79
|
+
|
|
80
|
+
`blitz activity` reads your coding agent's own local session transcripts
|
|
81
|
+
(currently Claude Code's, at `~/.claude/projects`) for the current repo and
|
|
82
|
+
replays each prompt alongside the actions taken to resolve it (tool calls,
|
|
83
|
+
sub-agents) and the final response:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
blitz activity --list # sessions for this repo, most recent first
|
|
87
|
+
blitz activity # the most recent session's timeline
|
|
88
|
+
blitz activity <session-id-prefix> # a specific session
|
|
89
|
+
blitz activity --full # untruncated prompts/responses/tool I/O
|
|
90
|
+
blitz activity --html report.html # also write a self-contained HTML report
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The terminal view is compact by default; `--full` shows untruncated text. The
|
|
94
|
+
HTML report includes each action's full tool input/output, collapsed per action.
|
|
95
|
+
Each turn shows its raw token usage and, where a pre-edit backup exists, a real
|
|
96
|
+
diff of what an `Edit`/`Write` changed. Point at a different transcript root with
|
|
97
|
+
`--sessions-dir`. This only reads local session data, no project id or API key.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
"""`blitz activity` — replay a
|
|
1
|
+
"""`blitz activity` — replay a coding-agent session: what you asked, and how it
|
|
2
2
|
was resolved.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
cwd>/<session-id>.jsonl (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
The currently-supported transcript format is Claude Code's, at
|
|
5
|
+
~/.claude/projects/<dash-encoded-cwd>/<session-id>.jsonl (an agent conversation
|
|
6
|
+
log, distinct from any captured LLM traces). One JSON object per line; the schema
|
|
7
|
+
isn't publicly documented, so this module errs on the side of skipping/ignoring
|
|
8
|
+
unrecognized shapes rather than raising. Other agents' local transcript formats
|
|
9
|
+
(Codex, OpenCode, ...) are not parsed yet; a future adapter would normalize them
|
|
10
|
+
into the same session/turn shape this module already produces.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
@@ -76,7 +77,8 @@ class SessionSummary:
|
|
|
76
77
|
turn_count: int
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
def
|
|
80
|
+
def default_sessions_dir() -> Path:
|
|
81
|
+
# Claude Code is the currently-supported transcript source.
|
|
80
82
|
return Path.home() / ".claude" / "projects"
|
|
81
83
|
|
|
82
84
|
|
|
@@ -92,8 +94,8 @@ def encode_project_path(path: Path) -> str:
|
|
|
92
94
|
return str(path.resolve()).replace("/", "-")
|
|
93
95
|
|
|
94
96
|
|
|
95
|
-
def resolve_project_dir(
|
|
96
|
-
candidate =
|
|
97
|
+
def resolve_project_dir(sessions_dir: Path, project_path: Path) -> Optional[Path]:
|
|
98
|
+
candidate = sessions_dir / encode_project_path(project_path)
|
|
97
99
|
return candidate if candidate.is_dir() else None
|
|
98
100
|
|
|
99
101
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Static code graph for the locker-room ontology.
|
|
2
|
+
|
|
3
|
+
Per repo, emits File/Symbol nodes and DEFINES/DECLARES/IMPORTS/CALLS edges so a
|
|
4
|
+
spec's git-diff lineage (Feature -TOUCHES-> File/Symbol) can be walked out into
|
|
5
|
+
the code that implements it. Python is parsed with stdlib ``ast`` (symbols +
|
|
6
|
+
imports + best-effort intra-repo calls); JS/TS get File + relative-IMPORTS only;
|
|
7
|
+
other recognized source files get a bare File node. A noise-dir prune list keeps
|
|
8
|
+
big repos cheap, and a per-file sha cache (owned by the store) means only changed
|
|
9
|
+
files are re-parsed on the periodic refresh.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Directories that never hold first-party source worth indexing (pruned in place
|
|
21
|
+
# so os.walk doesn't descend into them). Kept self-contained here so the code
|
|
22
|
+
# graph has no cross-feature dependency.
|
|
23
|
+
_PRUNE_DIRS = {
|
|
24
|
+
".git", ".hg", ".svn", "node_modules", "venv", ".venv", "env", ".env",
|
|
25
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache", "dist",
|
|
26
|
+
"build", ".next", ".nuxt", "out", "coverage", ".tox", "site-packages",
|
|
27
|
+
".idea", ".vscode",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_PY_EXT = {".py"}
|
|
31
|
+
_JS_EXT = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
32
|
+
# Other first-party source we index at file level only (no symbols/imports).
|
|
33
|
+
_OTHER_EXT = {
|
|
34
|
+
".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".h", ".cc", ".cpp",
|
|
35
|
+
".hpp", ".cs", ".swift", ".scala", ".sh", ".bash", ".sql", ".proto",
|
|
36
|
+
}
|
|
37
|
+
_MAX_FILE_BYTES_DEFAULT = 1_000_000
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lang_for(ext: str) -> str | None:
|
|
41
|
+
if ext in _PY_EXT:
|
|
42
|
+
return "python"
|
|
43
|
+
if ext in _JS_EXT:
|
|
44
|
+
return "js"
|
|
45
|
+
if ext in _OTHER_EXT:
|
|
46
|
+
return "other"
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _iter_files(repo: Path):
|
|
51
|
+
"""Yield (path, lang) for indexable files under repo, pruning noise dirs."""
|
|
52
|
+
for dirpath, dirnames, filenames in os.walk(repo):
|
|
53
|
+
dirnames[:] = [d for d in dirnames if d not in _PRUNE_DIRS and not d.startswith(".")]
|
|
54
|
+
for fn in filenames:
|
|
55
|
+
lang = _lang_for(Path(fn).suffix)
|
|
56
|
+
if lang:
|
|
57
|
+
yield Path(dirpath) / fn, lang
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def file_id(path: Path) -> str:
|
|
61
|
+
return f"file:{path.resolve()}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def symbol_id(path: Path, qualname: str) -> str:
|
|
65
|
+
return f"sym:{path.resolve()}:{qualname}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --- python extraction -------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_module(repo_root: Path, file_path: Path, module: str, level: int) -> Path | None:
|
|
72
|
+
"""Resolve a Python import target to a repo file (module.py or pkg/__init__.py)."""
|
|
73
|
+
if level and level > 0:
|
|
74
|
+
base = file_path.parent
|
|
75
|
+
for _ in range(level - 1):
|
|
76
|
+
base = base.parent
|
|
77
|
+
parts = module.split(".") if module else []
|
|
78
|
+
target = base.joinpath(*parts)
|
|
79
|
+
else:
|
|
80
|
+
parts = module.split(".") if module else []
|
|
81
|
+
if not parts:
|
|
82
|
+
return None
|
|
83
|
+
target = repo_root.joinpath(*parts)
|
|
84
|
+
for cand in (target.with_suffix(".py"), target / "__init__.py"):
|
|
85
|
+
if cand.is_file():
|
|
86
|
+
return cand
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _PyVisitor(ast.NodeVisitor):
|
|
91
|
+
def __init__(self, repo_root: Path, path: Path):
|
|
92
|
+
self.repo_root = repo_root
|
|
93
|
+
self.path = path
|
|
94
|
+
self.fid = file_id(path)
|
|
95
|
+
self.stack: list[str] = []
|
|
96
|
+
self.symbols: list[dict] = [] # {qualname,name,kind,lineno,endline,node,parent}
|
|
97
|
+
# alias name -> ("module", modfile) or ("symbol", modfile, realname)
|
|
98
|
+
self.aliases: dict[str, tuple] = {}
|
|
99
|
+
self.import_files: set[str] = set()
|
|
100
|
+
self.module_funcs: set[str] = set() # top-level def names in this file
|
|
101
|
+
|
|
102
|
+
# -- imports --
|
|
103
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
104
|
+
for a in node.names:
|
|
105
|
+
modfile = _resolve_module(self.repo_root, self.path, a.name, 0)
|
|
106
|
+
bound = (a.asname or a.name.split(".")[0])
|
|
107
|
+
if modfile:
|
|
108
|
+
self.aliases[bound] = ("module", modfile)
|
|
109
|
+
self.import_files.add(str(modfile.resolve()))
|
|
110
|
+
self.generic_visit(node)
|
|
111
|
+
|
|
112
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
113
|
+
modfile = _resolve_module(self.repo_root, self.path, node.module or "", node.level or 0)
|
|
114
|
+
if modfile:
|
|
115
|
+
self.import_files.add(str(modfile.resolve()))
|
|
116
|
+
for a in node.names:
|
|
117
|
+
if a.name == "*":
|
|
118
|
+
continue
|
|
119
|
+
bound = a.asname or a.name
|
|
120
|
+
self.aliases[bound] = ("symbol", modfile, a.name)
|
|
121
|
+
self.generic_visit(node)
|
|
122
|
+
|
|
123
|
+
# -- defs --
|
|
124
|
+
def _add_symbol(self, node, kind: str) -> None:
|
|
125
|
+
name = node.name
|
|
126
|
+
qual = ".".join(self.stack + [name])
|
|
127
|
+
parent = ".".join(self.stack) if self.stack else None
|
|
128
|
+
if not self.stack:
|
|
129
|
+
self.module_funcs.add(name)
|
|
130
|
+
self.symbols.append({
|
|
131
|
+
"qualname": qual, "name": name, "kind": kind,
|
|
132
|
+
"lineno": node.lineno, "endline": getattr(node, "end_lineno", node.lineno),
|
|
133
|
+
"node": node, "parent": parent,
|
|
134
|
+
})
|
|
135
|
+
self.stack.append(name)
|
|
136
|
+
self.generic_visit(node)
|
|
137
|
+
self.stack.pop()
|
|
138
|
+
|
|
139
|
+
def visit_FunctionDef(self, node) -> None:
|
|
140
|
+
self._add_symbol(node, "method" if self.stack else "function")
|
|
141
|
+
|
|
142
|
+
def visit_AsyncFunctionDef(self, node) -> None:
|
|
143
|
+
self._add_symbol(node, "method" if self.stack else "function")
|
|
144
|
+
|
|
145
|
+
def visit_ClassDef(self, node) -> None:
|
|
146
|
+
self._add_symbol(node, "class")
|
|
147
|
+
|
|
148
|
+
# -- calls (resolved after the tree is walked) --
|
|
149
|
+
def _resolve_call(self, call: ast.Call) -> str | None:
|
|
150
|
+
f = call.func
|
|
151
|
+
if isinstance(f, ast.Name):
|
|
152
|
+
got = self.aliases.get(f.id)
|
|
153
|
+
if got and got[0] == "symbol":
|
|
154
|
+
return symbol_id(got[1], got[2])
|
|
155
|
+
if f.id in self.module_funcs:
|
|
156
|
+
return symbol_id(self.path, f.id)
|
|
157
|
+
elif isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
|
|
158
|
+
got = self.aliases.get(f.value.id)
|
|
159
|
+
if got and got[0] == "module":
|
|
160
|
+
return symbol_id(got[1], f.attr)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def call_edges(self) -> list[tuple[str, str]]:
|
|
164
|
+
edges: list[tuple[str, str]] = []
|
|
165
|
+
for sym in self.symbols:
|
|
166
|
+
if sym["kind"] == "class":
|
|
167
|
+
continue
|
|
168
|
+
src = symbol_id(self.path, sym["qualname"])
|
|
169
|
+
for sub in ast.walk(sym["node"]):
|
|
170
|
+
if isinstance(sub, ast.Call):
|
|
171
|
+
tgt = self._resolve_call(sub)
|
|
172
|
+
if tgt and tgt != src:
|
|
173
|
+
edges.append((src, tgt))
|
|
174
|
+
return edges
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_python(repo_root: Path, path: Path, repo_id: str, text: str, lang: str) -> dict:
|
|
178
|
+
nodes: list[list] = [[file_id(path), "File",
|
|
179
|
+
{"path": str(path), "repo": str(repo_root), "lang": lang}]]
|
|
180
|
+
edges: list[list] = [[repo_id, file_id(path), "HAS_FILE", {}]]
|
|
181
|
+
try:
|
|
182
|
+
tree = ast.parse(text)
|
|
183
|
+
except (SyntaxError, ValueError):
|
|
184
|
+
return {"nodes": nodes, "edges": edges} # File node only
|
|
185
|
+
v = _PyVisitor(repo_root, path)
|
|
186
|
+
v.visit(tree)
|
|
187
|
+
for sym in v.symbols:
|
|
188
|
+
sid = symbol_id(path, sym["qualname"])
|
|
189
|
+
nodes.append([sid, "Symbol", {
|
|
190
|
+
"name": sym["name"], "qualname": sym["qualname"], "kind": sym["kind"],
|
|
191
|
+
"file": str(path), "lineno": sym["lineno"], "endline": sym["endline"],
|
|
192
|
+
}])
|
|
193
|
+
edges.append([file_id(path), sid, "DEFINES", {}])
|
|
194
|
+
if sym["parent"]:
|
|
195
|
+
edges.append([symbol_id(path, sym["parent"]), sid, "DECLARES", {}])
|
|
196
|
+
for imp in sorted(v.import_files):
|
|
197
|
+
edges.append([file_id(path), f"file:{imp}", "IMPORTS", {}])
|
|
198
|
+
for src, tgt in v.call_edges():
|
|
199
|
+
edges.append([src, tgt, "CALLS", {}])
|
|
200
|
+
return {"nodes": nodes, "edges": edges}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --- js / other extraction ---------------------------------------------------
|
|
204
|
+
|
|
205
|
+
import re
|
|
206
|
+
|
|
207
|
+
_JS_IMPORT = re.compile(r"""(?:import\s.*?from\s*|import\s*|require\s*\(\s*)['"]([^'"]+)['"]""")
|
|
208
|
+
_JS_RESOLVE_EXT = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _resolve_js(path: Path, spec: str) -> Path | None:
|
|
212
|
+
if not spec.startswith("."):
|
|
213
|
+
return None # only intra-repo relative imports
|
|
214
|
+
target = (path.parent / spec).resolve()
|
|
215
|
+
cands = [target] + [target.with_suffix(e) for e in _JS_RESOLVE_EXT]
|
|
216
|
+
cands += [target / ("index" + e) for e in _JS_RESOLVE_EXT]
|
|
217
|
+
for c in cands:
|
|
218
|
+
if c.is_file():
|
|
219
|
+
return c
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _extract_js(repo_root: Path, path: Path, repo_id: str, text: str, lang: str) -> dict:
|
|
224
|
+
nodes = [[file_id(path), "File", {"path": str(path), "repo": str(repo_root), "lang": lang}]]
|
|
225
|
+
edges = [[repo_id, file_id(path), "HAS_FILE", {}]]
|
|
226
|
+
seen: set[str] = set()
|
|
227
|
+
for m in _JS_IMPORT.finditer(text):
|
|
228
|
+
tgt = _resolve_js(path, m.group(1))
|
|
229
|
+
if tgt and str(tgt) not in seen:
|
|
230
|
+
seen.add(str(tgt))
|
|
231
|
+
edges.append([file_id(path), file_id(tgt), "IMPORTS", {}])
|
|
232
|
+
return {"nodes": nodes, "edges": edges}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _extract_other(repo_root: Path, path: Path, repo_id: str, text: str, lang: str) -> dict:
|
|
236
|
+
return {
|
|
237
|
+
"nodes": [[file_id(path), "File", {"path": str(path), "repo": str(repo_root), "lang": lang}]],
|
|
238
|
+
"edges": [[repo_id, file_id(path), "HAS_FILE", {}]],
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
_EXTRACTORS = {"python": _extract_python, "js": _extract_js, "other": _extract_other}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# --- driver ------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def scan_repo(store, repo: Path, repo_id: str, max_file_bytes: int = _MAX_FILE_BYTES_DEFAULT) -> None:
|
|
249
|
+
"""Extract the code graph for ``repo`` into ``store`` (with a per-file cache).
|
|
250
|
+
|
|
251
|
+
``store`` must provide file_cache_get/file_cache_put/file_cache_prune plus
|
|
252
|
+
add_node/add_edge (see OntologyStore). Only files whose content sha changed
|
|
253
|
+
since the last run are re-parsed; the rest replay cached rows.
|
|
254
|
+
"""
|
|
255
|
+
repo = repo.resolve()
|
|
256
|
+
visited: list[str] = []
|
|
257
|
+
for path, lang in _iter_files(repo):
|
|
258
|
+
try:
|
|
259
|
+
raw = path.read_bytes()
|
|
260
|
+
except OSError:
|
|
261
|
+
continue
|
|
262
|
+
if len(raw) > max_file_bytes:
|
|
263
|
+
continue
|
|
264
|
+
abspath = str(path.resolve())
|
|
265
|
+
visited.append(abspath)
|
|
266
|
+
sha = hashlib.sha1(raw).hexdigest()
|
|
267
|
+
cached = store.file_cache_get(abspath)
|
|
268
|
+
if cached and cached[0] == sha:
|
|
269
|
+
rows = json.loads(cached[1])
|
|
270
|
+
else:
|
|
271
|
+
text = raw.decode("utf-8", errors="replace")
|
|
272
|
+
rows = _EXTRACTORS[lang](repo, path, repo_id, text, lang)
|
|
273
|
+
store.file_cache_put(abspath, sha, json.dumps(rows))
|
|
274
|
+
for nid, label, props in rows["nodes"]:
|
|
275
|
+
store.add_node(nid, label, props)
|
|
276
|
+
for src, dst, rel, props in rows["edges"]:
|
|
277
|
+
store.add_edge(src, dst, rel, props)
|
|
278
|
+
store.file_cache_prune(str(repo), set(visited))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""Convert Claude Code activity (parsed by `_activity.py`) into a fine-tuning
|
|
2
|
-
corpus stored in SQLite, for the `blitz
|
|
2
|
+
corpus stored in SQLite, for the `blitz locker-room watch`
|
|
3
3
|
container.
|
|
4
4
|
|
|
5
5
|
This is a wholly separate store from `_local.py`'s `spans` table —
|
|
@@ -137,7 +137,7 @@ def turn_to_row(
|
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
class TurnStore:
|
|
140
|
-
"""Writer for the
|
|
140
|
+
"""Writer for the locker-room activity fine-tuning corpus. Unlike _local.py's
|
|
141
141
|
read-only LocalStore (which assumes an externally-stubbed db and returns
|
|
142
142
|
None if missing), this store owns its schema and creates it on first
|
|
143
143
|
connect — it's the one and only writer, structurally closer to
|
|
@@ -244,7 +244,7 @@ def ingest_once(projects_dir: Path, store: TurnStore, print_fn=print) -> int:
|
|
|
244
244
|
raw_text = session_path.read_text(encoding="utf-8", errors="replace")
|
|
245
245
|
session = _activity.parse_session(session_path)
|
|
246
246
|
except Exception as exc: # a malformed/mid-write file must never kill the watcher
|
|
247
|
-
print_fn(f"[
|
|
247
|
+
print_fn(f"[watch] skip {session_path}: {exc}")
|
|
248
248
|
continue
|
|
249
249
|
|
|
250
250
|
store.upsert_raw_session(
|