blitz-cli 0.6.0__tar.gz → 0.8.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 (102) hide show
  1. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/.github/workflows/publish.yml +5 -0
  2. blitz_cli-0.8.0/.gitignore +11 -0
  3. blitz_cli-0.8.0/PKG-INFO +118 -0
  4. blitz_cli-0.8.0/README.md +99 -0
  5. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/_activity.py +12 -10
  6. blitz_cli-0.8.0/blitz_cli/_code_graph.py +278 -0
  7. blitz_cli-0.6.0/blitz_cli/_codeassist.py → blitz_cli-0.8.0/blitz_cli/_corpus.py +3 -3
  8. blitz_cli-0.8.0/blitz_cli/_git.py +120 -0
  9. blitz_cli-0.8.0/blitz_cli/_ontology.py +434 -0
  10. blitz_cli-0.8.0/blitz_cli/_playbooks.py +267 -0
  11. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/_redact.py +2 -2
  12. blitz_cli-0.8.0/blitz_cli/_scaffold.py +137 -0
  13. blitz_cli-0.8.0/blitz_cli/_train.py +247 -0
  14. blitz_cli-0.8.0/blitz_cli/cli.py +388 -0
  15. blitz_cli-0.8.0/blitz_cli/playbooks/commands/__init__.py +0 -0
  16. blitz_cli-0.8.0/blitz_cli/playbooks/commands/analyze.md +255 -0
  17. blitz_cli-0.8.0/blitz_cli/playbooks/commands/clarify.md +285 -0
  18. blitz_cli-0.8.0/blitz_cli/playbooks/commands/implement.md +219 -0
  19. blitz_cli-0.8.0/blitz_cli/playbooks/commands/plan.md +169 -0
  20. blitz_cli-0.8.0/blitz_cli/playbooks/commands/playbook.md +152 -0
  21. blitz_cli-0.8.0/blitz_cli/playbooks/commands/specify.md +345 -0
  22. blitz_cli-0.8.0/blitz_cli/playbooks/commands/tasks.md +218 -0
  23. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/memory/playbook.md +50 -0
  24. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/check-prerequisites.sh +195 -0
  25. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/common.sh +692 -0
  26. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/create-new-feature.sh +301 -0
  27. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/setup-plan.sh +83 -0
  28. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/setup-tasks.sh +91 -0
  29. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/templates/plan-template.md +113 -0
  30. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/templates/spec-template.md +131 -0
  31. blitz_cli-0.8.0/blitz_cli/playbooks/scaffold/.playbooks/templates/tasks-template.md +252 -0
  32. blitz_cli-0.8.0/blitz_cli/playbooks/workspace/__init__.py +0 -0
  33. blitz_cli-0.8.0/blitz_cli/playbooks/workspace/huddle.md +101 -0
  34. blitz_cli-0.8.0/blitz_cli/templates/locker_room_Makefile.tmpl +40 -0
  35. blitz_cli-0.8.0/blitz_cli/templates/locker_room_README.md.tmpl +38 -0
  36. blitz_cli-0.8.0/blitz_cli/templates/locker_room_compose.yml.tmpl +69 -0
  37. blitz_cli-0.8.0/pyproject.toml +46 -0
  38. blitz_cli-0.8.0/tests/test_cli.py +52 -0
  39. blitz_cli-0.6.0/tests/test_codeassist.py → blitz_cli-0.8.0/tests/test_corpus.py +21 -21
  40. blitz_cli-0.8.0/tests/test_playbooks.py +98 -0
  41. blitz_cli-0.8.0/tests/test_scaffold.py +156 -0
  42. blitz_cli-0.6.0/.gitignore +0 -7
  43. blitz_cli-0.6.0/PKG-INFO +0 -95
  44. blitz_cli-0.6.0/README.md +0 -84
  45. blitz_cli-0.6.0/blitz_cli/_client.py +0 -197
  46. blitz_cli-0.6.0/blitz_cli/_local.py +0 -251
  47. blitz_cli-0.6.0/blitz_cli/_release.py +0 -396
  48. blitz_cli-0.6.0/blitz_cli/_scaffold.py +0 -197
  49. blitz_cli-0.6.0/blitz_cli/_scan/__init__.py +0 -71
  50. blitz_cli-0.6.0/blitz_cli/_scan/_classify.py +0 -28
  51. blitz_cli-0.6.0/blitz_cli/_scan/_js.py +0 -101
  52. blitz_cli-0.6.0/blitz_cli/_scan/_models.py +0 -132
  53. blitz_cli-0.6.0/blitz_cli/_scan/_python.py +0 -311
  54. blitz_cli-0.6.0/blitz_cli/_scan/_report.py +0 -205
  55. blitz_cli-0.6.0/blitz_cli/_scan/_walk.py +0 -68
  56. blitz_cli-0.6.0/blitz_cli/_skill.py +0 -59
  57. blitz_cli-0.6.0/blitz_cli/_sync.py +0 -129
  58. blitz_cli-0.6.0/blitz_cli/_toml.py +0 -63
  59. blitz_cli-0.6.0/blitz_cli/_traces.py +0 -194
  60. blitz_cli-0.6.0/blitz_cli/cli.py +0 -862
  61. blitz_cli-0.6.0/blitz_cli/skills/blitz-fix-teacher/SKILL.md +0 -180
  62. blitz_cli-0.6.0/blitz_cli/skills/blitz-instrument/SKILL.md +0 -167
  63. blitz_cli-0.6.0/blitz_cli/skills/blitz-setup-training/SKILL.md +0 -164
  64. blitz_cli-0.6.0/blitz_cli/skills/blitz-simulate-traces/SKILL.md +0 -148
  65. blitz_cli-0.6.0/blitz_cli/templates/Dockerfile.tmpl +0 -23
  66. blitz_cli-0.6.0/blitz_cli/templates/Makefile.tmpl +0 -79
  67. blitz_cli-0.6.0/blitz_cli/templates/README.md.tmpl +0 -84
  68. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Dockerfile.tmpl +0 -21
  69. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +0 -26
  70. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Makefile.tmpl +0 -75
  71. blitz_cli-0.6.0/blitz_cli/templates/codeassist_README.md.tmpl +0 -94
  72. blitz_cli-0.6.0/blitz_cli/templates/codeassist_dockerignore.tmpl +0 -9
  73. blitz_cli-0.6.0/blitz_cli/templates/codeassist_requirements.txt.tmpl +0 -4
  74. blitz_cli-0.6.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +0 -17
  75. blitz_cli-0.6.0/blitz_cli/templates/codeassist_train.py.tmpl +0 -255
  76. blitz_cli-0.6.0/blitz_cli/templates/dockerignore.tmpl +0 -9
  77. blitz_cli-0.6.0/blitz_cli/templates/requirements.txt.tmpl +0 -16
  78. blitz_cli-0.6.0/blitz_cli/templates/serve.py.tmpl +0 -125
  79. blitz_cli-0.6.0/blitz_cli/templates/train.py.tmpl +0 -182
  80. blitz_cli-0.6.0/blitz_cli/templates/watch.py.tmpl +0 -46
  81. blitz_cli-0.6.0/pyproject.toml +0 -33
  82. blitz_cli-0.6.0/tests/test_cli.py +0 -79
  83. blitz_cli-0.6.0/tests/test_local.py +0 -350
  84. blitz_cli-0.6.0/tests/test_release.py +0 -315
  85. blitz_cli-0.6.0/tests/test_scaffold.py +0 -357
  86. blitz_cli-0.6.0/tests/test_scan.py +0 -308
  87. blitz_cli-0.6.0/tests/test_serve.py +0 -140
  88. blitz_cli-0.6.0/tests/test_sync.py +0 -72
  89. blitz_cli-0.6.0/tests/test_toml.py +0 -122
  90. blitz_cli-0.6.0/tests/test_traces.py +0 -98
  91. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/.github/workflows/homebrew.yml +0 -0
  92. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/LICENSE +0 -0
  93. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/__init__.py +0 -0
  94. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/_dataset.py +0 -0
  95. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/_make.py +0 -0
  96. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/_tool_schemas.py +0 -0
  97. {blitz_cli-0.6.0/blitz_cli/skills → blitz_cli-0.8.0/blitz_cli/playbooks}/__init__.py +0 -0
  98. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/blitz_cli/templates/__init__.py +0 -0
  99. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/tests/test_activity.py +0 -0
  100. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/tests/test_make.py +0 -0
  101. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/tests/test_redact.py +0 -0
  102. {blitz_cli-0.6.0 → blitz_cli-0.8.0}/tests/test_tool_schemas.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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+
9
+ # stray test artifacts from running the CLI in-repo
10
+ /.claude/
11
+ /.playbooks/
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: blitz-cli
3
+ Version: 0.8.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: train
10
+ Requires-Dist: accelerate==1.2.1; extra == 'train'
11
+ Requires-Dist: datasets==3.2.0; extra == 'train'
12
+ Requires-Dist: liger-kernel==0.5.2; extra == 'train'
13
+ Requires-Dist: mlflow==2.19.0; extra == 'train'
14
+ Requires-Dist: peft==0.14.0; extra == 'train'
15
+ Requires-Dist: sentencepiece==0.2.0; extra == 'train'
16
+ Requires-Dist: transformers==4.48.0; extra == 'train'
17
+ Requires-Dist: trl==0.13.0; extra == 'train'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # blitz-cli
21
+
22
+ Developer CLI for the Blitz **locker-room**: spec-driven playbooks for your
23
+ coding agent, a local ontology graph of your codebase/huddles/features (served to
24
+ agents over MCP), and the composable containers that keep it fresh.
25
+
26
+ ```bash
27
+ pip install blitz-cli
28
+ ```
29
+
30
+ The core CLI is stdlib plus `rich`. Two optional extras carry heavier deps only
31
+ when you need them: `blitz-cli[locker-room]` (the MCP server) and
32
+ `blitz-cli[train]` (the QLoRA trainer).
33
+
34
+ Top-level commands: `playbooks`, `locker-room`, `activity`.
35
+
36
+ ## Playbooks: spec-driven development commands
37
+
38
+ `blitz playbooks init` installs a set of spec-driven-development slash-commands
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.
41
+
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
46
+ ```
47
+
48
+ The commands are `specify`, `plan`, `tasks`, `implement`, `playbook`, `clarify`,
49
+ and `analyze`. Run at a folder of repos, `init` installs into each repo it finds
50
+ (recursing through non-git folders) and adds a workspace-level `/huddle` command
51
+ for coordinating a feature across repos. Use `--ignore PATH` to skip a repo and
52
+ `--root` to force a single install.
53
+
54
+ ## Locker-room: ontology graph, watcher, trainer, MCP
55
+
56
+ The locker-room is a **context node**: a set of containers that ingest, build,
57
+ and serve an ontology of your codebase to agents (on your machine, a VPN, or a
58
+ team host). `blitz locker-room init` scaffolds a `docker-compose.yml` that pulls
59
+ the prebuilt node images (from `sparepartslabs/blitz-locker-room`) for four roles
60
+ 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 MCP (SSE) and a browser graph view.
66
+ - **train**: on-demand QLoRA trainer (needs a GPU).
67
+
68
+ ```bash
69
+ blitz locker-room init # write ./locker-room/docker-compose.yml
70
+ cd locker-room
71
+ blitz locker-room up # start watch + graph + mcp
72
+ blitz locker-room logs
73
+ ```
74
+
75
+ The CLI is just these static commands (`init`, and the `up`/`down`/`logs`/`pull`/
76
+ `train` lifecycle passthroughs). The runtime (ingest, graph build, serving) lives
77
+ in the node images, not the CLI.
78
+
79
+ ### The graph agents can query
80
+
81
+ The graph engine (a blitz-cli library the node runs) reuses playbook discovery and
82
+ static code parsing (Python symbols + imports + calls; File nodes for other
83
+ languages) to build `Workspace`, `Repo`, `AgentTool`, `Playbook`, `Feature` (with
84
+ lifecycle stage), `Huddle`, `File`, and `Symbol` nodes. Each feature's git branch
85
+ diff links it to the code it touches (`Feature -TOUCHES-> File/Symbol`), so an
86
+ agent can walk a spec out to the full lineage of code it implements, and back.
87
+
88
+ The `mcp` role serves this: point an agent's MCP config at
89
+ `http://<node>:8848/sse` (tools `list_repos`, `get_repo`, `list_huddles`,
90
+ `get_huddle`, `get_playbooks`, `get_lineage`, `impact_of`, `get_file`,
91
+ `get_symbol`, `neighbors`), or open `http://<node>:8848/graph` in a browser.
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,99 @@
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
+ The locker-room is a **context node**: a set of containers that ingest, build,
38
+ and serve an ontology of your codebase to agents (on your machine, a VPN, or a
39
+ team host). `blitz locker-room init` scaffolds a `docker-compose.yml` that pulls
40
+ the prebuilt node images (from `sparepartslabs/blitz-locker-room`) for four roles
41
+ sharing `./data`:
42
+
43
+ - **watch**: ingests your coding agent's session transcripts into a corpus.
44
+ - **graph**: rebuilds an ontology of your playbook-enabled repos, huddles,
45
+ features, and their code lineage into `data/ontology.db` on a loop.
46
+ - **mcp**: serves that ontology to agents over MCP (SSE) and a browser graph view.
47
+ - **train**: on-demand QLoRA trainer (needs a GPU).
48
+
49
+ ```bash
50
+ blitz locker-room init # write ./locker-room/docker-compose.yml
51
+ cd locker-room
52
+ blitz locker-room up # start watch + graph + mcp
53
+ blitz locker-room logs
54
+ ```
55
+
56
+ The CLI is just these static commands (`init`, and the `up`/`down`/`logs`/`pull`/
57
+ `train` lifecycle passthroughs). The runtime (ingest, graph build, serving) lives
58
+ in the node images, not the CLI.
59
+
60
+ ### The graph agents can query
61
+
62
+ The graph engine (a blitz-cli library the node runs) reuses playbook discovery and
63
+ static code parsing (Python symbols + imports + calls; File nodes for other
64
+ languages) to build `Workspace`, `Repo`, `AgentTool`, `Playbook`, `Feature` (with
65
+ lifecycle stage), `Huddle`, `File`, and `Symbol` nodes. Each feature's git branch
66
+ diff links it to the code it touches (`Feature -TOUCHES-> File/Symbol`), so an
67
+ agent can walk a spec out to the full lineage of code it implements, and back.
68
+
69
+ The `mcp` role serves this: point an agent's MCP config at
70
+ `http://<node>:8848/sse` (tools `list_repos`, `get_repo`, `list_huddles`,
71
+ `get_huddle`, `get_playbooks`, `get_lineage`, `impact_of`, `get_file`,
72
+ `get_symbol`, `neighbors`), or open `http://<node>:8848/graph` in a browser.
73
+
74
+ ### Training (optional)
75
+
76
+ `blitz locker-room train` runs an on-demand QLoRA fine-tune of the watcher corpus
77
+ in the trainer container (needs a GPU); adapters land in `./train-out`. Override
78
+ the base model with `BASE_MODEL_HF` or `blitz locker-room init --base-model`.
79
+
80
+ ## Activity: replay a coding-agent session
81
+
82
+ `blitz activity` reads your coding agent's own local session transcripts
83
+ (currently Claude Code's, at `~/.claude/projects`) for the current repo and
84
+ replays each prompt alongside the actions taken to resolve it (tool calls,
85
+ sub-agents) and the final response:
86
+
87
+ ```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
93
+ ```
94
+
95
+ The terminal view is compact by default; `--full` shows untruncated text. The
96
+ HTML report includes each action's full tool input/output, collapsed per action.
97
+ Each turn shows its raw token usage and, where a pre-edit backup exists, a real
98
+ diff of what an `Edit`/`Write` changed. Point at a different transcript root with
99
+ `--sessions-dir`. This only reads local session data, no project id or API key.
@@ -1,12 +1,13 @@
1
- """`blitz activity` — replay a Claude Code session: what you asked, and how it
1
+ """`blitz activity` — replay a coding-agent session: what you asked, and how it
2
2
  was resolved.
3
3
 
4
- Reads Claude Code's own transcript store at ~/.claude/projects/<dash-encoded-
5
- cwd>/<session-id>.jsonl (not related to blitz's own captured LLM traces in
6
- _local.py's traces.db this is Claude Code's conversation log, a different
7
- data source entirely). One JSON object per line; the schema isn't publicly
8
- documented, so this module errs on the side of skipping/ignoring unrecognized
9
- shapes rather than raising.
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 default_claude_projects_dir() -> Path:
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(claude_dir: Path, project_path: Path) -> Optional[Path]:
96
- candidate = claude_dir / encode_project_path(project_path)
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 scaffold code-assist` watcher
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 code-assist fine-tuning corpus. Unlike _local.py's
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"[code-assist] skip {session_path}: {exc}")
247
+ print_fn(f"[watch] skip {session_path}: {exc}")
248
248
  continue
249
249
 
250
250
  store.upsert_raw_session(