blitz-cli 0.6.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.
Files changed (103) hide show
  1. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/.github/workflows/publish.yml +5 -0
  2. blitz_cli-0.7.0/.gitignore +11 -0
  3. blitz_cli-0.7.0/PKG-INFO +118 -0
  4. blitz_cli-0.7.0/README.md +97 -0
  5. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/_activity.py +12 -10
  6. blitz_cli-0.7.0/blitz_cli/_code_graph.py +278 -0
  7. blitz_cli-0.6.0/blitz_cli/_codeassist.py → blitz_cli-0.7.0/blitz_cli/_corpus.py +3 -3
  8. blitz_cli-0.7.0/blitz_cli/_git.py +120 -0
  9. blitz_cli-0.7.0/blitz_cli/_locker_room_mcp.py +117 -0
  10. blitz_cli-0.7.0/blitz_cli/_ontology.py +417 -0
  11. blitz_cli-0.7.0/blitz_cli/_playbooks.py +267 -0
  12. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/_redact.py +2 -2
  13. blitz_cli-0.7.0/blitz_cli/_scaffold.py +137 -0
  14. blitz_cli-0.7.0/blitz_cli/_train.py +247 -0
  15. blitz_cli-0.7.0/blitz_cli/cli.py +525 -0
  16. blitz_cli-0.7.0/blitz_cli/playbooks/commands/__init__.py +0 -0
  17. blitz_cli-0.7.0/blitz_cli/playbooks/commands/analyze.md +255 -0
  18. blitz_cli-0.7.0/blitz_cli/playbooks/commands/clarify.md +285 -0
  19. blitz_cli-0.7.0/blitz_cli/playbooks/commands/implement.md +219 -0
  20. blitz_cli-0.7.0/blitz_cli/playbooks/commands/plan.md +169 -0
  21. blitz_cli-0.7.0/blitz_cli/playbooks/commands/playbook.md +152 -0
  22. blitz_cli-0.7.0/blitz_cli/playbooks/commands/specify.md +345 -0
  23. blitz_cli-0.7.0/blitz_cli/playbooks/commands/tasks.md +218 -0
  24. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/memory/playbook.md +50 -0
  25. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/check-prerequisites.sh +195 -0
  26. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/common.sh +692 -0
  27. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/create-new-feature.sh +301 -0
  28. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/setup-plan.sh +83 -0
  29. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/scripts/bash/setup-tasks.sh +91 -0
  30. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/templates/plan-template.md +113 -0
  31. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/templates/spec-template.md +131 -0
  32. blitz_cli-0.7.0/blitz_cli/playbooks/scaffold/.playbooks/templates/tasks-template.md +252 -0
  33. blitz_cli-0.7.0/blitz_cli/playbooks/workspace/__init__.py +0 -0
  34. blitz_cli-0.7.0/blitz_cli/playbooks/workspace/huddle.md +101 -0
  35. blitz_cli-0.7.0/blitz_cli/templates/locker_room_Makefile.tmpl +40 -0
  36. blitz_cli-0.7.0/blitz_cli/templates/locker_room_README.md.tmpl +39 -0
  37. blitz_cli-0.7.0/blitz_cli/templates/locker_room_compose.yml.tmpl +66 -0
  38. blitz_cli-0.7.0/pyproject.toml +49 -0
  39. blitz_cli-0.7.0/tests/test_cli.py +66 -0
  40. blitz_cli-0.6.0/tests/test_codeassist.py → blitz_cli-0.7.0/tests/test_corpus.py +21 -21
  41. blitz_cli-0.7.0/tests/test_playbooks.py +98 -0
  42. blitz_cli-0.7.0/tests/test_scaffold.py +155 -0
  43. blitz_cli-0.6.0/.gitignore +0 -7
  44. blitz_cli-0.6.0/PKG-INFO +0 -95
  45. blitz_cli-0.6.0/README.md +0 -84
  46. blitz_cli-0.6.0/blitz_cli/_client.py +0 -197
  47. blitz_cli-0.6.0/blitz_cli/_local.py +0 -251
  48. blitz_cli-0.6.0/blitz_cli/_release.py +0 -396
  49. blitz_cli-0.6.0/blitz_cli/_scaffold.py +0 -197
  50. blitz_cli-0.6.0/blitz_cli/_scan/__init__.py +0 -71
  51. blitz_cli-0.6.0/blitz_cli/_scan/_classify.py +0 -28
  52. blitz_cli-0.6.0/blitz_cli/_scan/_js.py +0 -101
  53. blitz_cli-0.6.0/blitz_cli/_scan/_models.py +0 -132
  54. blitz_cli-0.6.0/blitz_cli/_scan/_python.py +0 -311
  55. blitz_cli-0.6.0/blitz_cli/_scan/_report.py +0 -205
  56. blitz_cli-0.6.0/blitz_cli/_scan/_walk.py +0 -68
  57. blitz_cli-0.6.0/blitz_cli/_skill.py +0 -59
  58. blitz_cli-0.6.0/blitz_cli/_sync.py +0 -129
  59. blitz_cli-0.6.0/blitz_cli/_toml.py +0 -63
  60. blitz_cli-0.6.0/blitz_cli/_traces.py +0 -194
  61. blitz_cli-0.6.0/blitz_cli/cli.py +0 -862
  62. blitz_cli-0.6.0/blitz_cli/skills/blitz-fix-teacher/SKILL.md +0 -180
  63. blitz_cli-0.6.0/blitz_cli/skills/blitz-instrument/SKILL.md +0 -167
  64. blitz_cli-0.6.0/blitz_cli/skills/blitz-setup-training/SKILL.md +0 -164
  65. blitz_cli-0.6.0/blitz_cli/skills/blitz-simulate-traces/SKILL.md +0 -148
  66. blitz_cli-0.6.0/blitz_cli/templates/Dockerfile.tmpl +0 -23
  67. blitz_cli-0.6.0/blitz_cli/templates/Makefile.tmpl +0 -79
  68. blitz_cli-0.6.0/blitz_cli/templates/README.md.tmpl +0 -84
  69. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Dockerfile.tmpl +0 -21
  70. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Dockerfile.train.tmpl +0 -26
  71. blitz_cli-0.6.0/blitz_cli/templates/codeassist_Makefile.tmpl +0 -75
  72. blitz_cli-0.6.0/blitz_cli/templates/codeassist_README.md.tmpl +0 -94
  73. blitz_cli-0.6.0/blitz_cli/templates/codeassist_dockerignore.tmpl +0 -9
  74. blitz_cli-0.6.0/blitz_cli/templates/codeassist_requirements.txt.tmpl +0 -4
  75. blitz_cli-0.6.0/blitz_cli/templates/codeassist_requirements_train.txt.tmpl +0 -17
  76. blitz_cli-0.6.0/blitz_cli/templates/codeassist_train.py.tmpl +0 -255
  77. blitz_cli-0.6.0/blitz_cli/templates/dockerignore.tmpl +0 -9
  78. blitz_cli-0.6.0/blitz_cli/templates/requirements.txt.tmpl +0 -16
  79. blitz_cli-0.6.0/blitz_cli/templates/serve.py.tmpl +0 -125
  80. blitz_cli-0.6.0/blitz_cli/templates/train.py.tmpl +0 -182
  81. blitz_cli-0.6.0/blitz_cli/templates/watch.py.tmpl +0 -46
  82. blitz_cli-0.6.0/pyproject.toml +0 -33
  83. blitz_cli-0.6.0/tests/test_cli.py +0 -79
  84. blitz_cli-0.6.0/tests/test_local.py +0 -350
  85. blitz_cli-0.6.0/tests/test_release.py +0 -315
  86. blitz_cli-0.6.0/tests/test_scaffold.py +0 -357
  87. blitz_cli-0.6.0/tests/test_scan.py +0 -308
  88. blitz_cli-0.6.0/tests/test_serve.py +0 -140
  89. blitz_cli-0.6.0/tests/test_sync.py +0 -72
  90. blitz_cli-0.6.0/tests/test_toml.py +0 -122
  91. blitz_cli-0.6.0/tests/test_traces.py +0 -98
  92. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/.github/workflows/homebrew.yml +0 -0
  93. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/LICENSE +0 -0
  94. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/__init__.py +0 -0
  95. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/_dataset.py +0 -0
  96. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/_make.py +0 -0
  97. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/_tool_schemas.py +0 -0
  98. {blitz_cli-0.6.0/blitz_cli/skills → blitz_cli-0.7.0/blitz_cli/playbooks}/__init__.py +0 -0
  99. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/blitz_cli/templates/__init__.py +0 -0
  100. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/tests/test_activity.py +0 -0
  101. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/tests/test_make.py +0 -0
  102. {blitz_cli-0.6.0 → blitz_cli-0.7.0}/tests/test_redact.py +0 -0
  103. {blitz_cli-0.6.0 → blitz_cli-0.7.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.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 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(