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

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