blitz-cli 0.13.0__tar.gz → 0.15.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.13.0 → blitz_cli-0.15.0}/PKG-INFO +35 -4
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/README.md +34 -3
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/__init__.py +1 -1
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_collect.py +129 -3
- blitz_cli-0.15.0/blitz_cli/_collect_http.py +241 -0
- blitz_cli-0.15.0/blitz_cli/_collect_server.py +217 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_execute.py +14 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_train.py +15 -15
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/cli.py +114 -17
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/implement.md +8 -1
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/templates/locker_room_Makefile.tmpl +1 -1
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/templates/locker_room_README.md.tmpl +5 -5
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/templates/locker_room_compose.yml.tmpl +27 -14
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/pyproject.toml +1 -1
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/checklists/requirements.md +35 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/contracts/client-sink.md +41 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/contracts/ingest-api.md +51 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/data-model.md +83 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/plan.md +108 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/quickstart.md +60 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/research.md +123 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/spec.md +137 -0
- blitz_cli-0.15.0/specs/005-collect-ingest-endpoint/tasks.md +105 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_cli.py +1 -1
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_collect.py +2 -2
- blitz_cli-0.15.0/tests/test_collect_http.py +315 -0
- blitz_cli-0.15.0/tests/test_collect_server.py +239 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_scaffold.py +8 -7
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/memory/playbook.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/scripts/bash/check-prerequisites.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/scripts/bash/common.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/scripts/bash/create-new-feature.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/scripts/bash/setup-plan.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/scripts/bash/setup-tasks.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/templates/plan-template.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/templates/spec-template.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.blitz/templates/tasks-template.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.github/workflows/homebrew.yml +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.github/workflows/publish.yml +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.gitignore +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/.mcp.json +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/LICENSE +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_activity.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_code_graph.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_corpus.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_dataset.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_git.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_gitignore.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_make.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_ontology.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_playbooks.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_redact.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_scaffold.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/_tool_schemas.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/__init__.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/__init__.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/analyze.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/clarify.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/plan.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/playbook.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/specify.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/commands/tasks.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/memory/playbook.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/check-prerequisites.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/common.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/create-new-feature.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-plan.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/scripts/bash/setup-tasks.sh +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/templates/plan-template.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/templates/spec-template.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/scaffold/.blitz/templates/tasks-template.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/workspace/__init__.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/playbooks/workspace/huddle.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/templates/__init__.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/blitz_cli/templates/workspace_playbook.md.tmpl +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/checklists/requirements.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/contracts/cli-commands.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/plan.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/quickstart.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/research.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/spec.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/001-cli-model-namespace/tasks.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/checklists/requirements.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/contracts/experimental-labels.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/data-model.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/plan.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/quickstart.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/research.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/spec.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/002-label-experimental-surfaces/tasks.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/checklists/requirements.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/contracts/cli-output.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/contracts/ensure-rules.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/contracts/gitignore-block.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/data-model.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/plan.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/quickstart.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/research.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/spec.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/003-init-gitignore-rules/tasks.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/checklists/requirements.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/contracts/cli.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/contracts/export-format.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/data-model.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/plan.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/quickstart.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/research.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/spec.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/specs/004-dpo-collect-pairs/tasks.md +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_activity.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_corpus.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_execute.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_gitignore.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_make.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_ontology.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_playbooks.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_redact.py +0 -0
- {blitz_cli-0.13.0 → blitz_cli-0.15.0}/tests/test_tool_schemas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blitz-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.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
|
|
@@ -75,6 +75,37 @@ Tasks whose text says "optional manual" are skipped and left unchecked. The
|
|
|
75
75
|
Ollama endpoint comes from `--ollama-url` or `OLLAMA_HOST`; the model from
|
|
76
76
|
`--model` or `BLITZ_EXECUTE_MODEL`.
|
|
77
77
|
|
|
78
|
+
### Collection over the ingest endpoint
|
|
79
|
+
|
|
80
|
+
`blitz model execute --collect` records every generation attempt as DPO
|
|
81
|
+
training data. By default it writes straight to the activity SQLite database
|
|
82
|
+
(`BLITZ_ACTIVITY_DB_PATH`, else `data/activity.db`). With `BLITZ_COLLECT_ENDPOINT` set,
|
|
83
|
+
the same writes go over HTTP JSON to a collect ingest server instead, so the
|
|
84
|
+
collector needs no filesystem access to the database:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# on the locker-room node (default port 1739, binds all interfaces, no auth):
|
|
88
|
+
BLITZ_ACTIVITY_DB_PATH=data/activity.db blitz locker-room collect-serve
|
|
89
|
+
|
|
90
|
+
# on the collecting machine:
|
|
91
|
+
BLITZ_COLLECT_ENDPOINT=http://locker-room-host:1739 blitz model execute --collect
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Each attempt is acknowledged by the endpoint before the next model call
|
|
95
|
+
starts. If the endpoint is unreachable when the run starts, the run fails
|
|
96
|
+
fast, same as an unwritable database. If it fails mid-run, the run keeps
|
|
97
|
+
going: undelivered payloads are buffered to `collect-buffer.jsonl` in the run
|
|
98
|
+
directory and the final output names the file. Deliver them later with
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
blitz model collect flush .blitz/execute/runs/<stamp>/collect-buffer.jsonl
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Flushing is idempotent (the server deduplicates attempts on their
|
|
105
|
+
run/task/sample/attempt identity), so re-running a flush, or flushing rows the
|
|
106
|
+
server already received, never duplicates data. `pairs extract/export/stats`
|
|
107
|
+
read the database directly, unaffected by the endpoint.
|
|
108
|
+
|
|
78
109
|
## Locker-room: ontology graph, watcher, trainer, MCP
|
|
79
110
|
|
|
80
111
|
The locker-room is a **context node**: a set of containers that ingest, build,
|
|
@@ -110,9 +141,9 @@ diff links it to the code it touches (`Feature -TOUCHES-> File/Symbol`), so an
|
|
|
110
141
|
agent can walk a spec out to the full lineage of code it implements, and back.
|
|
111
142
|
|
|
112
143
|
The `mcp` role serves this: point an agent's MCP config at
|
|
113
|
-
`http://<node>:
|
|
144
|
+
`http://<node>:1738/sse` (tools `list_repos`, `get_repo`, `list_huddles`,
|
|
114
145
|
`get_huddle`, `get_playbooks`, `get_lineage`, `impact_of`, `get_file`,
|
|
115
|
-
`get_symbol`, `neighbors`), or open `http://<node>:
|
|
146
|
+
`get_symbol`, `neighbors`), or open `http://<node>:1738/graph` in a browser.
|
|
116
147
|
|
|
117
148
|
### Training (optional)
|
|
118
149
|
|
|
@@ -120,7 +151,7 @@ The `mcp` role serves this: point an agent's MCP config at
|
|
|
120
151
|
|
|
121
152
|
`blitz locker-room train` runs an on-demand QLoRA fine-tune of the watcher corpus
|
|
122
153
|
in the trainer container (needs a GPU); adapters land in `./train-out`. Override
|
|
123
|
-
the base model with `
|
|
154
|
+
the base model with `BLITZ_BASE_MODEL_HF` or `blitz locker-room init --base-model`.
|
|
124
155
|
|
|
125
156
|
## Activity: replay a coding-agent session
|
|
126
157
|
|
|
@@ -56,6 +56,37 @@ Tasks whose text says "optional manual" are skipped and left unchecked. The
|
|
|
56
56
|
Ollama endpoint comes from `--ollama-url` or `OLLAMA_HOST`; the model from
|
|
57
57
|
`--model` or `BLITZ_EXECUTE_MODEL`.
|
|
58
58
|
|
|
59
|
+
### Collection over the ingest endpoint
|
|
60
|
+
|
|
61
|
+
`blitz model execute --collect` records every generation attempt as DPO
|
|
62
|
+
training data. By default it writes straight to the activity SQLite database
|
|
63
|
+
(`BLITZ_ACTIVITY_DB_PATH`, else `data/activity.db`). With `BLITZ_COLLECT_ENDPOINT` set,
|
|
64
|
+
the same writes go over HTTP JSON to a collect ingest server instead, so the
|
|
65
|
+
collector needs no filesystem access to the database:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# on the locker-room node (default port 1739, binds all interfaces, no auth):
|
|
69
|
+
BLITZ_ACTIVITY_DB_PATH=data/activity.db blitz locker-room collect-serve
|
|
70
|
+
|
|
71
|
+
# on the collecting machine:
|
|
72
|
+
BLITZ_COLLECT_ENDPOINT=http://locker-room-host:1739 blitz model execute --collect
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each attempt is acknowledged by the endpoint before the next model call
|
|
76
|
+
starts. If the endpoint is unreachable when the run starts, the run fails
|
|
77
|
+
fast, same as an unwritable database. If it fails mid-run, the run keeps
|
|
78
|
+
going: undelivered payloads are buffered to `collect-buffer.jsonl` in the run
|
|
79
|
+
directory and the final output names the file. Deliver them later with
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
blitz model collect flush .blitz/execute/runs/<stamp>/collect-buffer.jsonl
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Flushing is idempotent (the server deduplicates attempts on their
|
|
86
|
+
run/task/sample/attempt identity), so re-running a flush, or flushing rows the
|
|
87
|
+
server already received, never duplicates data. `pairs extract/export/stats`
|
|
88
|
+
read the database directly, unaffected by the endpoint.
|
|
89
|
+
|
|
59
90
|
## Locker-room: ontology graph, watcher, trainer, MCP
|
|
60
91
|
|
|
61
92
|
The locker-room is a **context node**: a set of containers that ingest, build,
|
|
@@ -91,9 +122,9 @@ diff links it to the code it touches (`Feature -TOUCHES-> File/Symbol`), so an
|
|
|
91
122
|
agent can walk a spec out to the full lineage of code it implements, and back.
|
|
92
123
|
|
|
93
124
|
The `mcp` role serves this: point an agent's MCP config at
|
|
94
|
-
`http://<node>:
|
|
125
|
+
`http://<node>:1738/sse` (tools `list_repos`, `get_repo`, `list_huddles`,
|
|
95
126
|
`get_huddle`, `get_playbooks`, `get_lineage`, `impact_of`, `get_file`,
|
|
96
|
-
`get_symbol`, `neighbors`), or open `http://<node>:
|
|
127
|
+
`get_symbol`, `neighbors`), or open `http://<node>:1738/graph` in a browser.
|
|
97
128
|
|
|
98
129
|
### Training (optional)
|
|
99
130
|
|
|
@@ -101,7 +132,7 @@ The `mcp` role serves this: point an agent's MCP config at
|
|
|
101
132
|
|
|
102
133
|
`blitz locker-room train` runs an on-demand QLoRA fine-tune of the watcher corpus
|
|
103
134
|
in the trainer container (needs a GPU); adapters land in `./train-out`. Override
|
|
104
|
-
the base model with `
|
|
135
|
+
the base model with `BLITZ_BASE_MODEL_HF` or `blitz locker-room init --base-model`.
|
|
105
136
|
|
|
106
137
|
## Activity: replay a coding-agent session
|
|
107
138
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
`CollectStore` owns four tables (`collect_runs`, `collect_attempts`,
|
|
5
5
|
`dpo_pairs`, `sft_queue`) in the same locker-room activity database that
|
|
6
6
|
`_corpus.py`'s `TurnStore` writes its transcript corpus to (default
|
|
7
|
-
`data/activity.db`, env override `
|
|
7
|
+
`data/activity.db`, env override `BLITZ_ACTIVITY_DB_PATH`, WAL journal). Like
|
|
8
8
|
`TurnStore`, it is the one writer of its tables and creates its schema on
|
|
9
9
|
first connect.
|
|
10
10
|
|
|
@@ -95,10 +95,38 @@ CREATE TABLE IF NOT EXISTS sft_queue (
|
|
|
95
95
|
"""
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
# Sentinel spec value for stub runs the ingest server creates when a write
|
|
99
|
+
# references a run_id it has no row for (for example the server restarted onto
|
|
100
|
+
# a fresh database mid-run). Flushes must survive server-side data loss, so
|
|
101
|
+
# ingestion never rejects a write for an unknown run; the sentinel keeps
|
|
102
|
+
# recovered runs visible in stats output.
|
|
103
|
+
RECOVERED_SPEC = "__recovered__"
|
|
104
|
+
|
|
105
|
+
# collect_runs metadata columns in insert order, shared by insert/ingest paths
|
|
106
|
+
# and by the ingest server's payload validation (payload fields mirror these
|
|
107
|
+
# column names 1:1; started_at/finished_at are server-owned).
|
|
108
|
+
RUN_FIELDS = (
|
|
109
|
+
"run_id", "repo_path", "spec", "tasks_file", "model", "endpoint",
|
|
110
|
+
"temperature", "max_retries", "k",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# collect_attempts client-sent columns. attempt_id and created_at are
|
|
114
|
+
# server-owned; env_failure/judge_* are judge-side and never client-sent.
|
|
115
|
+
ATTEMPT_REQUIRED_FIELDS = (
|
|
116
|
+
"run_id", "task_id", "sample_index", "attempt_index",
|
|
117
|
+
"prompt_messages_json", "reply", "actions_json", "passed",
|
|
118
|
+
)
|
|
119
|
+
ATTEMPT_OPTIONAL_FIELDS = (
|
|
120
|
+
"error_detail", "guard_violation", "input_tokens", "output_tokens",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
SFT_FIELDS = ("run_id", "task_id", "prompt_messages_json")
|
|
124
|
+
|
|
125
|
+
|
|
98
126
|
def default_db_path() -> Path:
|
|
99
|
-
"""The activity database path:
|
|
127
|
+
"""The activity database path: BLITZ_ACTIVITY_DB_PATH env override, else the
|
|
100
128
|
conventional data/activity.db relative to the CWD (same idiom as _train.py)."""
|
|
101
|
-
return Path(os.environ.get("
|
|
129
|
+
return Path(os.environ.get("BLITZ_ACTIVITY_DB_PATH", "data/activity.db"))
|
|
102
130
|
|
|
103
131
|
|
|
104
132
|
def _now_iso() -> str:
|
|
@@ -225,6 +253,104 @@ class CollectStore:
|
|
|
225
253
|
finally:
|
|
226
254
|
conn.close()
|
|
227
255
|
|
|
256
|
+
# -- Ingest-server write path ---------------------------------------
|
|
257
|
+
# These methods back the collect ingest endpoint (_collect_server.py).
|
|
258
|
+
# They differ from the insert_* methods above in two ways: every write
|
|
259
|
+
# is idempotent (retries, flush re-runs, and flush-after-partial-delivery
|
|
260
|
+
# are no-ops), and the server stamps its own timestamps. Payload shapes
|
|
261
|
+
# mirror the column names 1:1 (RUN_FIELDS / ATTEMPT_*_FIELDS / SFT_FIELDS).
|
|
262
|
+
|
|
263
|
+
def _ensure_run(self, conn: sqlite3.Connection, run_id: str) -> None:
|
|
264
|
+
"""Auto-create a recovered stub run when run_id has no row, so writes
|
|
265
|
+
referencing a run the server never saw are accepted, never rejected.
|
|
266
|
+
The stub is marked with the RECOVERED_SPEC sentinel."""
|
|
267
|
+
conn.execute(
|
|
268
|
+
"INSERT OR IGNORE INTO collect_runs (run_id, repo_path, spec, "
|
|
269
|
+
"tasks_file, model, endpoint, temperature, max_retries, k, started_at) "
|
|
270
|
+
"VALUES (?, '', ?, '', '', '', 0.0, 0, 0, ?)",
|
|
271
|
+
(run_id, RECOVERED_SPEC, _now_iso()),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def ingest_run(self, payload: dict) -> bool:
|
|
275
|
+
"""Idempotent run-open: INSERT OR IGNORE on the run_id primary key,
|
|
276
|
+
started_at stamped by this server. Returns True when the row was
|
|
277
|
+
created, False when the run_id already existed (no changes made)."""
|
|
278
|
+
conn = self._connect()
|
|
279
|
+
try:
|
|
280
|
+
cur = conn.execute(
|
|
281
|
+
"INSERT OR IGNORE INTO collect_runs (run_id, repo_path, spec, "
|
|
282
|
+
"tasks_file, model, endpoint, temperature, max_retries, k, "
|
|
283
|
+
"started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
284
|
+
tuple(payload[f] for f in RUN_FIELDS) + (_now_iso(),),
|
|
285
|
+
)
|
|
286
|
+
conn.commit()
|
|
287
|
+
return bool(cur.rowcount)
|
|
288
|
+
finally:
|
|
289
|
+
conn.close()
|
|
290
|
+
|
|
291
|
+
def ingest_attempts(self, rows: list) -> tuple:
|
|
292
|
+
"""Idempotent batch attempt ingestion in one transaction: INSERT OR
|
|
293
|
+
IGNORE on the natural key (run_id, task_id, sample_index,
|
|
294
|
+
attempt_index), created_at stamped by this server, unknown run_ids
|
|
295
|
+
recovered via a stub run. Returns (inserted, duplicates)."""
|
|
296
|
+
inserted = duplicates = 0
|
|
297
|
+
conn = self._connect()
|
|
298
|
+
try:
|
|
299
|
+
for row in rows:
|
|
300
|
+
self._ensure_run(conn, row["run_id"])
|
|
301
|
+
cur = conn.execute(
|
|
302
|
+
"INSERT OR IGNORE INTO collect_attempts (run_id, task_id, "
|
|
303
|
+
"sample_index, attempt_index, prompt_messages_json, reply, "
|
|
304
|
+
"actions_json, passed, error_detail, guard_violation, "
|
|
305
|
+
"input_tokens, output_tokens, created_at) "
|
|
306
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
307
|
+
tuple(row[f] for f in ATTEMPT_REQUIRED_FIELDS)
|
|
308
|
+
+ tuple(row.get(f) for f in ATTEMPT_OPTIONAL_FIELDS)
|
|
309
|
+
+ (_now_iso(),),
|
|
310
|
+
)
|
|
311
|
+
if cur.rowcount:
|
|
312
|
+
inserted += 1
|
|
313
|
+
else:
|
|
314
|
+
duplicates += 1
|
|
315
|
+
conn.commit()
|
|
316
|
+
return inserted, duplicates
|
|
317
|
+
finally:
|
|
318
|
+
conn.close()
|
|
319
|
+
|
|
320
|
+
def finish_run_if_open(self, run_id: str) -> None:
|
|
321
|
+
"""Replay-safe finish: stamps finished_at only while it is NULL, so a
|
|
322
|
+
flushed or retried finish marker never moves an existing stamp.
|
|
323
|
+
Unknown run_ids get a recovered stub run first."""
|
|
324
|
+
conn = self._connect()
|
|
325
|
+
try:
|
|
326
|
+
self._ensure_run(conn, run_id)
|
|
327
|
+
conn.execute(
|
|
328
|
+
"UPDATE collect_runs SET finished_at = ? "
|
|
329
|
+
"WHERE run_id = ? AND finished_at IS NULL",
|
|
330
|
+
(_now_iso(), run_id),
|
|
331
|
+
)
|
|
332
|
+
conn.commit()
|
|
333
|
+
finally:
|
|
334
|
+
conn.close()
|
|
335
|
+
|
|
336
|
+
def ingest_sft(self, payload: dict) -> bool:
|
|
337
|
+
"""Idempotent sft placeholder ingestion (UNIQUE(run_id, task_id)),
|
|
338
|
+
created_at stamped by this server, unknown run_ids recovered.
|
|
339
|
+
Returns True when a row was created, False when already present."""
|
|
340
|
+
conn = self._connect()
|
|
341
|
+
try:
|
|
342
|
+
self._ensure_run(conn, payload["run_id"])
|
|
343
|
+
cur = conn.execute(
|
|
344
|
+
"INSERT OR IGNORE INTO sft_queue "
|
|
345
|
+
"(run_id, task_id, prompt_messages_json, created_at) "
|
|
346
|
+
"VALUES (?, ?, ?, ?)",
|
|
347
|
+
tuple(payload[f] for f in SFT_FIELDS) + (_now_iso(),),
|
|
348
|
+
)
|
|
349
|
+
conn.commit()
|
|
350
|
+
return bool(cur.rowcount)
|
|
351
|
+
finally:
|
|
352
|
+
conn.close()
|
|
353
|
+
|
|
228
354
|
|
|
229
355
|
def extract_pairs(store: CollectStore, run_id: Optional[str] = None) -> dict:
|
|
230
356
|
"""Materialize eligible DPO preference pairs from collected attempts.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""HTTP collection sink and buffer/flush tooling for the collect ingest
|
|
2
|
+
endpoint (see _collect_server.py for the server side).
|
|
3
|
+
|
|
4
|
+
`HttpCollectSink` is the BLITZ_COLLECT_ENDPOINT-selected counterpart of
|
|
5
|
+
`_collect.CollectStore`: it exposes the same methods the execute loop calls
|
|
6
|
+
(check_writable, insert_run, insert_attempt, finish_run,
|
|
7
|
+
insert_sft_placeholder) but delivers each payload synchronously over HTTP, so
|
|
8
|
+
an attempt is durable at the endpoint before the next model call starts.
|
|
9
|
+
Stdlib only (urllib), per the repo's runtime dependency constraint.
|
|
10
|
+
|
|
11
|
+
Failure semantics (contracts/client-sink.md): an unreachable endpoint at run
|
|
12
|
+
start fails fast via check_writable (OSError, same failure class as an
|
|
13
|
+
unwritable database). Once the run has started, the first delivery failure of
|
|
14
|
+
any kind flips the sink into buffered mode: the failed payload and every
|
|
15
|
+
subsequent one (attempts, sft placeholders, the finish marker) are appended as
|
|
16
|
+
JSONL to <rundir>/collect-buffer.jsonl, the run never fails because of the
|
|
17
|
+
sink, and buffer_notice() tells the user what was buffered and how to flush
|
|
18
|
+
it. `blitz model collect flush <buffer-file>` replays the records in order;
|
|
19
|
+
the server's idempotent writes make retries and re-flushes duplicate-free.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Callable, Optional
|
|
29
|
+
|
|
30
|
+
BUFFER_FILENAME = "collect-buffer.jsonl"
|
|
31
|
+
|
|
32
|
+
_TIMEOUT = 10 # seconds per request; a hung sink must not stall model calls long
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def post_json(endpoint: str, path: str, payload: dict, timeout: int = _TIMEOUT) -> dict:
|
|
36
|
+
"""POST payload as JSON to endpoint+path and return the parsed JSON reply.
|
|
37
|
+
Raises OSError (URLError subclasses it) on connection failure or timeout
|
|
38
|
+
and urllib.error.HTTPError (also an OSError) on any non-2xx status."""
|
|
39
|
+
req = urllib.request.Request(
|
|
40
|
+
endpoint.rstrip("/") + path,
|
|
41
|
+
data=json.dumps(payload).encode(),
|
|
42
|
+
headers={"Content-Type": "application/json"},
|
|
43
|
+
)
|
|
44
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
45
|
+
return json.load(resp)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _path_for(kind: str, payload: dict) -> str:
|
|
49
|
+
if kind == "run":
|
|
50
|
+
return "/v1/collect/runs"
|
|
51
|
+
if kind == "attempts":
|
|
52
|
+
return "/v1/collect/attempts"
|
|
53
|
+
if kind == "finish":
|
|
54
|
+
return f"/v1/collect/runs/{payload['run_id']}/finish"
|
|
55
|
+
if kind == "sft":
|
|
56
|
+
return "/v1/collect/sft"
|
|
57
|
+
raise ValueError(f"unknown buffer record kind {kind!r}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HttpCollectSink:
|
|
61
|
+
"""Collection sink that writes through the ingest endpoint instead of
|
|
62
|
+
opening the activity database. Duck-types CollectStore's writer surface;
|
|
63
|
+
selected by cli._cmd_execute when BLITZ_COLLECT_ENDPOINT is set."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, endpoint: str, timeout: int = _TIMEOUT):
|
|
66
|
+
self.endpoint = endpoint.rstrip("/")
|
|
67
|
+
self._timeout = timeout
|
|
68
|
+
self._buffer_path: Optional[Path] = None
|
|
69
|
+
self._failed = False
|
|
70
|
+
self._first_error: Optional[str] = None
|
|
71
|
+
self._buffered_count = 0
|
|
72
|
+
|
|
73
|
+
# -- run-start reachability check (same failure class as an unwritable db)
|
|
74
|
+
|
|
75
|
+
def check_writable(self) -> None:
|
|
76
|
+
"""GET /v1/health; raises OSError when the endpoint is unreachable or
|
|
77
|
+
unhealthy so the run fails fast before any task executes."""
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(
|
|
80
|
+
self.endpoint + "/v1/health", timeout=self._timeout
|
|
81
|
+
) as resp:
|
|
82
|
+
json.load(resp)
|
|
83
|
+
except OSError:
|
|
84
|
+
raise
|
|
85
|
+
except Exception as exc: # non-JSON body etc.
|
|
86
|
+
raise OSError(f"collect endpoint health check failed: {exc}")
|
|
87
|
+
|
|
88
|
+
# -- execute-loop hooks (discovered with getattr; CollectStore has neither)
|
|
89
|
+
|
|
90
|
+
def bind_rundir(self, rundir: Path) -> None:
|
|
91
|
+
"""Called by _execute.run once the run directory exists; the JSONL
|
|
92
|
+
buffer for undeliverable payloads lives there."""
|
|
93
|
+
self._buffer_path = Path(rundir) / BUFFER_FILENAME
|
|
94
|
+
|
|
95
|
+
def buffer_notice(self) -> Optional[str]:
|
|
96
|
+
"""A user-facing message when payloads were buffered, else None."""
|
|
97
|
+
if not self._buffered_count:
|
|
98
|
+
return None
|
|
99
|
+
return (
|
|
100
|
+
f"collect: endpoint {self.endpoint} failed mid-run "
|
|
101
|
+
f"({self._first_error}); {self._buffered_count} payload(s) were "
|
|
102
|
+
f"buffered to {self._buffer_path}. The run itself is unaffected. "
|
|
103
|
+
f"Deliver them later with: blitz model collect flush {self._buffer_path}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# -- delivery core
|
|
107
|
+
|
|
108
|
+
def _deliver(self, kind: str, payload: dict) -> None:
|
|
109
|
+
"""Send one payload, or buffer it. After the first failure everything
|
|
110
|
+
goes to the buffer: this preserves delivery order in the file and
|
|
111
|
+
avoids paying a connection timeout on every remaining model call."""
|
|
112
|
+
if not self._failed:
|
|
113
|
+
try:
|
|
114
|
+
post_json(self.endpoint, _path_for(kind, payload), payload,
|
|
115
|
+
timeout=self._timeout)
|
|
116
|
+
return
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
self._failed = True
|
|
119
|
+
self._first_error = f"{type(exc).__name__}: {exc}"
|
|
120
|
+
self._buffer(kind, payload)
|
|
121
|
+
|
|
122
|
+
def _buffer(self, kind: str, payload: dict) -> None:
|
|
123
|
+
if self._buffer_path is None:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
"HttpCollectSink cannot buffer before bind_rundir() is called"
|
|
126
|
+
)
|
|
127
|
+
self._buffer_path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
with self._buffer_path.open("a", encoding="utf-8") as f:
|
|
129
|
+
f.write(json.dumps({"kind": kind, "payload": payload},
|
|
130
|
+
ensure_ascii=False) + "\n")
|
|
131
|
+
self._buffered_count += 1
|
|
132
|
+
|
|
133
|
+
# -- CollectStore writer surface
|
|
134
|
+
|
|
135
|
+
def insert_run(
|
|
136
|
+
self,
|
|
137
|
+
run_id: str,
|
|
138
|
+
repo_path: str,
|
|
139
|
+
spec: str,
|
|
140
|
+
tasks_file: str,
|
|
141
|
+
model: str,
|
|
142
|
+
endpoint: str,
|
|
143
|
+
temperature: float,
|
|
144
|
+
max_retries: int,
|
|
145
|
+
k: int,
|
|
146
|
+
) -> None:
|
|
147
|
+
self._deliver("run", {
|
|
148
|
+
"run_id": run_id, "repo_path": repo_path, "spec": spec,
|
|
149
|
+
"tasks_file": tasks_file, "model": model, "endpoint": endpoint,
|
|
150
|
+
"temperature": temperature, "max_retries": max_retries, "k": k,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
def insert_attempt(
|
|
154
|
+
self,
|
|
155
|
+
run_id: str,
|
|
156
|
+
task_id: str,
|
|
157
|
+
sample_index: int,
|
|
158
|
+
attempt_index: int,
|
|
159
|
+
prompt_messages_json: str,
|
|
160
|
+
reply: str,
|
|
161
|
+
actions_json: str,
|
|
162
|
+
passed: int,
|
|
163
|
+
error_detail: Optional[str] = None,
|
|
164
|
+
guard_violation: Optional[str] = None,
|
|
165
|
+
input_tokens: Optional[int] = None,
|
|
166
|
+
output_tokens: Optional[int] = None,
|
|
167
|
+
) -> int:
|
|
168
|
+
self._deliver("attempts", {"attempts": [{
|
|
169
|
+
"run_id": run_id, "task_id": task_id,
|
|
170
|
+
"sample_index": sample_index, "attempt_index": attempt_index,
|
|
171
|
+
"prompt_messages_json": prompt_messages_json, "reply": reply,
|
|
172
|
+
"actions_json": actions_json, "passed": passed,
|
|
173
|
+
"error_detail": error_detail, "guard_violation": guard_violation,
|
|
174
|
+
"input_tokens": input_tokens, "output_tokens": output_tokens,
|
|
175
|
+
}]})
|
|
176
|
+
# The direct store returns the autoincrement attempt_id; the execute
|
|
177
|
+
# loop ignores it, and over HTTP the id is server-side only.
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
def finish_run(self, run_id: str) -> None:
|
|
181
|
+
self._deliver("finish", {"run_id": run_id})
|
|
182
|
+
|
|
183
|
+
def insert_sft_placeholder(
|
|
184
|
+
self, run_id: str, task_id: str, prompt_messages_json: str
|
|
185
|
+
) -> None:
|
|
186
|
+
self._deliver("sft", {
|
|
187
|
+
"run_id": run_id, "task_id": task_id,
|
|
188
|
+
"prompt_messages_json": prompt_messages_json,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def flush_buffer(
|
|
193
|
+
buffer_path: Path,
|
|
194
|
+
endpoint: str,
|
|
195
|
+
print_fn: Callable = print,
|
|
196
|
+
timeout: int = _TIMEOUT,
|
|
197
|
+
) -> int:
|
|
198
|
+
"""Deliver every record of a collect buffer file, in file order, to the
|
|
199
|
+
ingest endpoint. Returns 0 on full delivery, 1 on the first failure (the
|
|
200
|
+
file is left intact either way; server-side idempotency makes re-runs and
|
|
201
|
+
partial overlaps duplicate-free). Reports delivered attempt counts."""
|
|
202
|
+
buffer_path = Path(buffer_path)
|
|
203
|
+
records = []
|
|
204
|
+
with buffer_path.open("r", encoding="utf-8") as f:
|
|
205
|
+
for n, line in enumerate(f, 1):
|
|
206
|
+
line = line.strip()
|
|
207
|
+
if not line:
|
|
208
|
+
continue
|
|
209
|
+
try:
|
|
210
|
+
rec = json.loads(line)
|
|
211
|
+
kind, payload = rec["kind"], rec["payload"]
|
|
212
|
+
_path_for(kind, payload) # validates the kind
|
|
213
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
214
|
+
print_fn(f"error: {buffer_path}:{n} is not a valid buffer record: {exc}")
|
|
215
|
+
return 1
|
|
216
|
+
records.append((n, kind, payload))
|
|
217
|
+
|
|
218
|
+
if not records:
|
|
219
|
+
print_fn(f"{buffer_path}: nothing to flush")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
inserted = duplicates = 0
|
|
223
|
+
for n, kind, payload in records:
|
|
224
|
+
try:
|
|
225
|
+
reply = post_json(endpoint, _path_for(kind, payload), payload,
|
|
226
|
+
timeout=timeout)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
print_fn(
|
|
229
|
+
f"error: delivery failed at {buffer_path}:{n} ({kind}): {exc}. "
|
|
230
|
+
"The buffer file is unchanged; re-run the flush once the "
|
|
231
|
+
"endpoint is reachable (idempotent, no duplicates)."
|
|
232
|
+
)
|
|
233
|
+
return 1
|
|
234
|
+
if kind == "attempts":
|
|
235
|
+
inserted += reply.get("inserted", 0)
|
|
236
|
+
duplicates += reply.get("duplicates", 0)
|
|
237
|
+
print_fn(
|
|
238
|
+
f"flushed {len(records)} record(s) from {buffer_path}: "
|
|
239
|
+
f"{inserted} attempt(s) inserted, {duplicates} duplicate(s) ignored"
|
|
240
|
+
)
|
|
241
|
+
return 0
|