turnloop 0.1.0__tar.gz → 0.1.2__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.
- {turnloop-0.1.0 → turnloop-0.1.2}/.github/workflows/ci.yml +2 -2
- {turnloop-0.1.0 → turnloop-0.1.2}/PKG-INFO +88 -35
- {turnloop-0.1.0 → turnloop-0.1.2}/README.md +85 -32
- {turnloop-0.1.0 → turnloop-0.1.2}/pyproject.toml +8 -3
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_config.py +256 -201
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_experiments.py +27 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_loop.py +11 -9
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_providers.py +64 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_tools_files.py +27 -0
- turnloop-0.1.2/tests/test_websearch.py +160 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/__init__.py +1 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/agent/loop.py +17 -6
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/cli.py +241 -202
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/config.py +16 -4
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/core/messages.py +14 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/diagnostics.py +6 -2
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/runner.py +4 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/mcp/client.py +4 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/permissions/engine.py +3 -3
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/anthropic.py +6 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/base.py +5 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/gemini.py +5 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/openai_compat.py +16 -4
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/sessions/store.py +1 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/base.py +5 -1
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/builtin.py +2 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/read.py +50 -3
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/runner.py +17 -12
- turnloop-0.1.2/turnloop/tools/websearch.py +318 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/.gitignore +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/.turnloop/.gitignore +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/conftest.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/fixtures/anthropic_tool_call.sse +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/fixtures/glm_tool_call.sse +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/fixtures/mcp_echo_server.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_bash.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_compaction.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_hooks_mcp_commands.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_permissions.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/tests/test_tui.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/__main__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/agent/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/agent/factory.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/agent/headless.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/agent/subagent.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/agent/system_prompt.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/commands/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/commands/dispatch.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/commands/loader.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/context/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/context/budget.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/context/compaction.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/context/memory.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/core/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/core/events.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/core/ids.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/core/tokens.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/errors.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/bakeoff.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/ceiling-check.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/context-glm.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/context.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/glm-selfhosted.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/groq-free.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/hard-calibration.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/hard-glm.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/loop.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/nvidia-smoke.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/reliability.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/rewrite-calibration.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/configs/smoke.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/graders.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/report.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/default.yaml +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_1.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_2.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_3.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_4.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_5.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_6.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_7.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/bulky/module_8.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/circular_import/models.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/circular_import/services.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cli_flag/app.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/empty/.keep +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/failing_test/calc.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/failing_test/test_calc.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/generate_bulky.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/grep_edit/ingest.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/grep_edit/load.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/grep_edit/transform.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/needle/inventory.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/needle/pricing.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/needle/shipping.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/regression_trap/validators.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/rename/billing.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/rename/report.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/rename/test_billing.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/spec/parser.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/spec/test_parser.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/experiments/suite/fixtures/util/util.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/hooks/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/hooks/runner.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/mcp/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/mcp/adapter.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/permissions/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/permissions/rules.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/mock.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/pricing.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/registry.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/providers/sse.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/sessions/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/sessions/models.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/ask.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/bash.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/edit.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/glob.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/grep.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/shell.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/skill.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/task.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/textio.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/todo.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/webfetch.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tools/write.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/app.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/app.tcss +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/bridge.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/widgets/__init__.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/widgets/permission.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/widgets/status.py +0 -0
- {turnloop-0.1.0 → turnloop-0.1.2}/turnloop/tui/widgets/transcript.py +0 -0
|
@@ -16,9 +16,9 @@ jobs:
|
|
|
16
16
|
python: ["3.11", "3.13"]
|
|
17
17
|
runs-on: ${{ matrix.os }}
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@v5
|
|
20
20
|
|
|
21
|
-
- uses: actions/setup-python@
|
|
21
|
+
- uses: actions/setup-python@v6
|
|
22
22
|
with:
|
|
23
23
|
python-version: ${{ matrix.python }}
|
|
24
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: turnloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A local-first agentic coding harness: streaming tool loop, permission gating, context compaction, subagents, and any LLM provider.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Pranesh-2005/turnloop
|
|
6
6
|
Author: Pranesh
|
|
@@ -20,11 +20,11 @@ Requires-Dist: pydantic>=2.7
|
|
|
20
20
|
Requires-Dist: pyyaml>=6.0
|
|
21
21
|
Requires-Dist: textual>=0.80
|
|
22
22
|
Provides-Extra: dev
|
|
23
|
-
Requires-Dist: mypy
|
|
23
|
+
Requires-Dist: mypy<3,>=2.1; extra == 'dev'
|
|
24
24
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
25
|
Requires-Dist: pytest-textual-snapshot>=1.0; extra == 'dev'
|
|
26
26
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
-
Requires-Dist: ruff
|
|
27
|
+
Requires-Dist: ruff<0.17,>=0.16.1; extra == 'dev'
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
30
|
# turnloop
|
|
@@ -38,18 +38,18 @@ OpenAI-compatible endpoint, and a **self-hosted GLM-5.2 (744B MoE, W4A16) on 4×
|
|
|
38
38
|
that this project treats as a first-class target rather than an afterthought.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
pip install
|
|
41
|
+
pip install turnloop
|
|
42
42
|
|
|
43
43
|
tl # TUI, mock provider, zero cost
|
|
44
44
|
tl -p "fix the failing test" # headless, one turn, prints and exits
|
|
45
45
|
tl --provider glm # the self-hosted endpoint
|
|
46
46
|
tl doctor # diagnose shell, providers, context budget
|
|
47
|
-
tl experiment run
|
|
47
|
+
tl experiment run smoke # a measurement run, offline and free
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Four runtime dependencies: `textual`, `pydantic`, `httpx`, `pyyaml`. No vendor
|
|
51
51
|
SDKs, no agent framework — the loop is the point, so the loop is written here.
|
|
52
|
-
~18,000 lines of Python,
|
|
52
|
+
~18,000 lines of Python, 249 tests, no network in the default test run.
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -94,16 +94,25 @@ something.
|
|
|
94
94
|
|
|
95
95
|
## Install
|
|
96
96
|
|
|
97
|
-
Requires Python ≥ 3.11.
|
|
97
|
+
Requires Python ≥ 3.11. Four dependencies, no compiler, no GPU needed to run it.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install turnloop
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
That is the whole install. It works offline against the `mock` provider immediately —
|
|
104
|
+
no API key required to try it.
|
|
105
|
+
|
|
106
|
+
From source, for development:
|
|
98
107
|
|
|
99
108
|
```bash
|
|
100
109
|
git clone https://github.com/Pranesh-2005/turnloop
|
|
101
110
|
cd turnloop
|
|
102
|
-
pip install -e .
|
|
103
|
-
pip install -e ".[dev]" # plus pytest, ruff, mypy
|
|
111
|
+
pip install -e ".[dev]" # editable, plus pytest, ruff, mypy
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
|
|
114
|
+
Either way you get two console scripts, `turnloop` and `tl`. They are the same entry
|
|
115
|
+
point.
|
|
107
116
|
|
|
108
117
|
Verify:
|
|
109
118
|
|
|
@@ -203,18 +212,36 @@ Custom commands live in `.turnloop/commands/*.md`.
|
|
|
203
212
|
- **`TURNLOOP.md`** at the project root for standing instructions (`CLAUDE.md` and
|
|
204
213
|
`AGENTS.md` are also read)
|
|
205
214
|
- **Text files of any kind** via the `Read` tool — source, JSON, YAML, CSV, Markdown
|
|
215
|
+
- **Images** — PNG, JPEG, GIF and WEBP via `Read`, detected by magic bytes rather
|
|
216
|
+
than extension, on providers whose `Capabilities.supports_vision` is true
|
|
217
|
+
(Anthropic, OpenAI, Gemini). On a text-only provider `Read` refuses the image and
|
|
218
|
+
names the provider, instead of sending a payload the model cannot parse — which
|
|
219
|
+
matters here, because GLM-5.2 W4A16 as deployed is text-only.
|
|
206
220
|
|
|
207
|
-
**Not supported:**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
because `Bash` exists: `pdftotext spec.pdf -` and the agent will reach for it itself.
|
|
221
|
+
**Not supported:** binary files other than those image formats — refused by a
|
|
222
|
+
null-byte sniff, which covers PDFs, `.docx` and archives. Anything convertible on
|
|
223
|
+
the command line comes back in scope, because `Bash` exists: `pdftotext spec.pdf -`
|
|
224
|
+
and the agent will reach for it itself.
|
|
212
225
|
|
|
213
226
|
### Tools
|
|
214
227
|
|
|
215
|
-
|
|
216
|
-
`WebFetch` `AskUserQuestion
|
|
217
|
-
|
|
228
|
+
Eleven registered: `Read` `Write` `Edit` `Glob` `Grep` `Bash` `TodoWrite` `Task`
|
|
229
|
+
`WebFetch` `WebSearch` `AskUserQuestion`, plus `Skill` when the project has skills.
|
|
230
|
+
Schemas are generated from pydantic models, so the declared schema and the
|
|
231
|
+
validation are the same object and cannot drift.
|
|
232
|
+
|
|
233
|
+
`WebSearch` and `WebFetch` are the pair: search finds URLs, fetch reads one. Both
|
|
234
|
+
are `read_only` and `parallel_safe`, so they work in plan mode and run concurrently
|
|
235
|
+
with `Read`/`Grep`. **Search needs no API key** — the default DuckDuckGo backend
|
|
236
|
+
works on a fresh install with an empty config. Brave and Tavily are opt-in upgrades:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{ "search": { "backend": "brave", "api_key_env": "BRAVE_API_KEY" } }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A keyed backend only errors if you actually select one, so the zero-config path
|
|
243
|
+
never breaks. Results are budgeted for a small window — 10 results maximum,
|
|
244
|
+
200-character snippets, 4k characters total, roughly 1k tokens against GLM's 65k.
|
|
218
245
|
|
|
219
246
|
---
|
|
220
247
|
|
|
@@ -225,8 +252,8 @@ turnloop/
|
|
|
225
252
|
core/ messages, events, token estimation, ids
|
|
226
253
|
providers/ anthropic · openai_compat (GLM/Groq/NVIDIA/vLLM) · gemini · mock
|
|
227
254
|
+ sse parser, pricing/capability presets, registry
|
|
228
|
-
tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch
|
|
229
|
-
+ runner, shell scanner, text io
|
|
255
|
+
tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch WebSearch
|
|
256
|
+
Ask Skill + runner, shell scanner, text io
|
|
230
257
|
permissions/ rule grammar, matcher, engine
|
|
231
258
|
agent/ loop, system prompt, subagents, factory, headless
|
|
232
259
|
context/ budget, three-tier compaction, memory files
|
|
@@ -364,7 +391,7 @@ All nine ship preconfigured. `doctor` shows which have keys present.
|
|
|
364
391
|
| `groq` | openai_compat | `openai/gpt-oss-120b` | `GROQ_API_KEY` | free tier: 8k TPM |
|
|
365
392
|
| `anthropic` | anthropic | `claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | |
|
|
366
393
|
| `openai` | openai_compat | `gpt-4.1` | `OPENAI_API_KEY` | |
|
|
367
|
-
| `gemini` | gemini | `gemini-
|
|
394
|
+
| `gemini` | gemini | `gemini-flash-latest` | `GEMINI_API_KEY` | alias, not a pin — see below |
|
|
368
395
|
| `blaxel` | openai_compat | `gpt-4o-mini` | `BL_API_KEY` | sandbox; ignores the model field |
|
|
369
396
|
| `ollama` | openai_compat | `qwen2.5-coder:14b` | none | local |
|
|
370
397
|
|
|
@@ -393,6 +420,15 @@ smoke tests, not for experiments.
|
|
|
393
420
|
NVIDIA model would silently inherit the self-hosted 65k window and $18.16/hr cost
|
|
394
421
|
basis. A regression test guards it.
|
|
395
422
|
|
|
423
|
+
**Gemini's default model is an alias because pinned ones rot in two different ways.**
|
|
424
|
+
0.1.1 shipped `gemini-2.5-pro`, whose free-tier quota is literally zero: a brand-new
|
|
425
|
+
key gets `429 ... generate_content_free_tier_input_token_count, limit: 0` on its first
|
|
426
|
+
request, which reads as a broken adapter rather than a model that was never free.
|
|
427
|
+
Pinning `gemini-2.5-flash` instead fails the other way — `404 no longer available to
|
|
428
|
+
new users`. `gemini-flash-latest` follows Google's own pointer. Worth knowing when
|
|
429
|
+
reading a 429 from any provider: the status code says "slow down", the body says
|
|
430
|
+
whether slowing down will ever help.
|
|
431
|
+
|
|
396
432
|
---
|
|
397
433
|
|
|
398
434
|
## Adding a provider
|
|
@@ -549,11 +585,16 @@ the 12 minutes of loading weights, so the two numbers do not match by design.
|
|
|
549
585
|
## Experiments
|
|
550
586
|
|
|
551
587
|
```bash
|
|
552
|
-
tl experiment run
|
|
553
|
-
tl experiment run
|
|
588
|
+
tl experiment run smoke # free, offline — a shipped config, by name
|
|
589
|
+
tl experiment run bakeoff # real models, real money
|
|
590
|
+
tl experiment run ./my-config.yaml # or your own, by path
|
|
554
591
|
tl experiment report .turnloop/experiments/<run-dir>
|
|
555
592
|
```
|
|
556
593
|
|
|
594
|
+
A bare name resolves against the configs bundled in the package, so the shipped
|
|
595
|
+
experiments are runnable straight from a `pip install`. A local file always wins, so
|
|
596
|
+
your own `smoke.yaml` is never shadowed by ours.
|
|
597
|
+
|
|
557
598
|
Thirteen configs ship, in four families, one runner:
|
|
558
599
|
|
|
559
600
|
- **`bakeoff.yaml`** — same tasks across GLM-5.2, Claude, GPT, Gemini, Groq.
|
|
@@ -821,7 +862,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
|
|
|
821
862
|
## Testing
|
|
822
863
|
|
|
823
864
|
```bash
|
|
824
|
-
pytest #
|
|
865
|
+
pytest # 249 tests, no network
|
|
825
866
|
ruff check turnloop
|
|
826
867
|
mypy turnloop
|
|
827
868
|
```
|
|
@@ -829,14 +870,15 @@ mypy turnloop
|
|
|
829
870
|
| file | tests | |
|
|
830
871
|
|---|---|---|
|
|
831
872
|
| `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
|
|
832
|
-
| `test_experiments.py` |
|
|
833
|
-
| `test_providers.py` |
|
|
834
|
-
| `test_tools_files.py` |
|
|
873
|
+
| `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
|
|
874
|
+
| `test_providers.py` | 31 | adapters against recorded `.sse` fixtures, image block encoding |
|
|
875
|
+
| `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
|
|
835
876
|
| `test_loop.py` | 20 | streaming, truncation, iteration cap |
|
|
836
877
|
| `test_hooks_mcp_commands.py` | 20 | lifecycle hooks, MCP client, slash commands |
|
|
837
878
|
| `test_config.py` | 19 | layering, env overrides, capability presets |
|
|
838
879
|
| `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
|
|
839
880
|
| `test_bash.py` | 14 | shell selection, process-tree kill |
|
|
881
|
+
| `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
|
|
840
882
|
| `test_tui.py` | 8 | Textual snapshots |
|
|
841
883
|
|
|
842
884
|
`conftest.py` monkeypatches httpx's transport to raise unless a test is marked
|
|
@@ -875,7 +917,8 @@ sessions wrongly.
|
|
|
875
917
|
|
|
876
918
|
API keys come from the environment or a `.env` at the project root. A real environment
|
|
877
919
|
variable always wins over the file, and `doctor` reports which key *names* were loaded
|
|
878
|
-
— never their values.
|
|
920
|
+
— never their values. This applies to `search.api_key_env` exactly as it does to a
|
|
921
|
+
provider's: settings files name the variable, never the secret.
|
|
879
922
|
|
|
880
923
|
Project instructions go in `TURNLOOP.md` (`CLAUDE.md` and `AGENTS.md` are also read).
|
|
881
924
|
Slash commands live in `.turnloop/commands/*.md`, skills in
|
|
@@ -893,13 +936,23 @@ just because it is on disk.
|
|
|
893
936
|
|
|
894
937
|
Stated rather than buried:
|
|
895
938
|
|
|
896
|
-
- **
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
-
|
|
900
|
-
- **
|
|
901
|
-
|
|
902
|
-
|
|
939
|
+
- **Image support is verified on two of three encoders.** The `openai_compat` data-URL
|
|
940
|
+
path and Gemini's `inline_data` path both round-trip a real image live; Anthropic's
|
|
941
|
+
is still fixture-only. None of it can be exercised on the primary target, since
|
|
942
|
+
GLM-5.2 W4A16 as deployed is text-only.
|
|
943
|
+
- **No binary file support** beyond those image formats. Null-byte sniff refuses the
|
|
944
|
+
rest. Convert via `Bash`.
|
|
945
|
+
- **`WebSearch`'s default backend parses HTML, not an API.** DuckDuckGo will rate-limit
|
|
946
|
+
and will eventually change its markup; both are handled as distinct, actionable
|
|
947
|
+
errors rather than a crash, but a keyed backend is the durable choice for anything
|
|
948
|
+
that matters. That trade is deliberate: the default has to work with no key at all.
|
|
949
|
+
- **The Anthropic adapter is fixture-tested but has never sent a real request.** It is
|
|
950
|
+
coded against the documented wire format and passes against recorded SSE, which is not
|
|
951
|
+
the same as verified. It is now the only one: the Gemini adapter is verified live
|
|
952
|
+
(text, tool calls and images on `gemini-flash-latest`), and OpenAI needs no separate
|
|
953
|
+
verification because `openai` is `kind="openai_compat"` — the same adapter Groq,
|
|
954
|
+
NVIDIA, Ollama, Blaxel and the self-hosted GLM endpoint all run through, making it
|
|
955
|
+
the most exercised code path in the repo.
|
|
903
956
|
- **`subtle_spec_edge` currently passes for nobody** (0/5 on gpt-4o-mini). The rewrite
|
|
904
957
|
overshot — runs die on collection errors and leave `NotImplementedError`, which is
|
|
905
958
|
failure-to-produce-working-code, not falling for the decoy. It needs another pass.
|
|
@@ -9,18 +9,18 @@ OpenAI-compatible endpoint, and a **self-hosted GLM-5.2 (744B MoE, W4A16) on 4×
|
|
|
9
9
|
that this project treats as a first-class target rather than an afterthought.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pip install
|
|
12
|
+
pip install turnloop
|
|
13
13
|
|
|
14
14
|
tl # TUI, mock provider, zero cost
|
|
15
15
|
tl -p "fix the failing test" # headless, one turn, prints and exits
|
|
16
16
|
tl --provider glm # the self-hosted endpoint
|
|
17
17
|
tl doctor # diagnose shell, providers, context budget
|
|
18
|
-
tl experiment run
|
|
18
|
+
tl experiment run smoke # a measurement run, offline and free
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Four runtime dependencies: `textual`, `pydantic`, `httpx`, `pyyaml`. No vendor
|
|
22
22
|
SDKs, no agent framework — the loop is the point, so the loop is written here.
|
|
23
|
-
~18,000 lines of Python,
|
|
23
|
+
~18,000 lines of Python, 249 tests, no network in the default test run.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -65,16 +65,25 @@ something.
|
|
|
65
65
|
|
|
66
66
|
## Install
|
|
67
67
|
|
|
68
|
-
Requires Python ≥ 3.11.
|
|
68
|
+
Requires Python ≥ 3.11. Four dependencies, no compiler, no GPU needed to run it.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install turnloop
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That is the whole install. It works offline against the `mock` provider immediately —
|
|
75
|
+
no API key required to try it.
|
|
76
|
+
|
|
77
|
+
From source, for development:
|
|
69
78
|
|
|
70
79
|
```bash
|
|
71
80
|
git clone https://github.com/Pranesh-2005/turnloop
|
|
72
81
|
cd turnloop
|
|
73
|
-
pip install -e .
|
|
74
|
-
pip install -e ".[dev]" # plus pytest, ruff, mypy
|
|
82
|
+
pip install -e ".[dev]" # editable, plus pytest, ruff, mypy
|
|
75
83
|
```
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
Either way you get two console scripts, `turnloop` and `tl`. They are the same entry
|
|
86
|
+
point.
|
|
78
87
|
|
|
79
88
|
Verify:
|
|
80
89
|
|
|
@@ -174,18 +183,36 @@ Custom commands live in `.turnloop/commands/*.md`.
|
|
|
174
183
|
- **`TURNLOOP.md`** at the project root for standing instructions (`CLAUDE.md` and
|
|
175
184
|
`AGENTS.md` are also read)
|
|
176
185
|
- **Text files of any kind** via the `Read` tool — source, JSON, YAML, CSV, Markdown
|
|
186
|
+
- **Images** — PNG, JPEG, GIF and WEBP via `Read`, detected by magic bytes rather
|
|
187
|
+
than extension, on providers whose `Capabilities.supports_vision` is true
|
|
188
|
+
(Anthropic, OpenAI, Gemini). On a text-only provider `Read` refuses the image and
|
|
189
|
+
names the provider, instead of sending a payload the model cannot parse — which
|
|
190
|
+
matters here, because GLM-5.2 W4A16 as deployed is text-only.
|
|
177
191
|
|
|
178
|
-
**Not supported:**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
because `Bash` exists: `pdftotext spec.pdf -` and the agent will reach for it itself.
|
|
192
|
+
**Not supported:** binary files other than those image formats — refused by a
|
|
193
|
+
null-byte sniff, which covers PDFs, `.docx` and archives. Anything convertible on
|
|
194
|
+
the command line comes back in scope, because `Bash` exists: `pdftotext spec.pdf -`
|
|
195
|
+
and the agent will reach for it itself.
|
|
183
196
|
|
|
184
197
|
### Tools
|
|
185
198
|
|
|
186
|
-
|
|
187
|
-
`WebFetch` `AskUserQuestion
|
|
188
|
-
|
|
199
|
+
Eleven registered: `Read` `Write` `Edit` `Glob` `Grep` `Bash` `TodoWrite` `Task`
|
|
200
|
+
`WebFetch` `WebSearch` `AskUserQuestion`, plus `Skill` when the project has skills.
|
|
201
|
+
Schemas are generated from pydantic models, so the declared schema and the
|
|
202
|
+
validation are the same object and cannot drift.
|
|
203
|
+
|
|
204
|
+
`WebSearch` and `WebFetch` are the pair: search finds URLs, fetch reads one. Both
|
|
205
|
+
are `read_only` and `parallel_safe`, so they work in plan mode and run concurrently
|
|
206
|
+
with `Read`/`Grep`. **Search needs no API key** — the default DuckDuckGo backend
|
|
207
|
+
works on a fresh install with an empty config. Brave and Tavily are opt-in upgrades:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{ "search": { "backend": "brave", "api_key_env": "BRAVE_API_KEY" } }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A keyed backend only errors if you actually select one, so the zero-config path
|
|
214
|
+
never breaks. Results are budgeted for a small window — 10 results maximum,
|
|
215
|
+
200-character snippets, 4k characters total, roughly 1k tokens against GLM's 65k.
|
|
189
216
|
|
|
190
217
|
---
|
|
191
218
|
|
|
@@ -196,8 +223,8 @@ turnloop/
|
|
|
196
223
|
core/ messages, events, token estimation, ids
|
|
197
224
|
providers/ anthropic · openai_compat (GLM/Groq/NVIDIA/vLLM) · gemini · mock
|
|
198
225
|
+ sse parser, pricing/capability presets, registry
|
|
199
|
-
tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch
|
|
200
|
-
+ runner, shell scanner, text io
|
|
226
|
+
tools/ Read Edit Write Bash Glob Grep TodoWrite Task WebFetch WebSearch
|
|
227
|
+
Ask Skill + runner, shell scanner, text io
|
|
201
228
|
permissions/ rule grammar, matcher, engine
|
|
202
229
|
agent/ loop, system prompt, subagents, factory, headless
|
|
203
230
|
context/ budget, three-tier compaction, memory files
|
|
@@ -335,7 +362,7 @@ All nine ship preconfigured. `doctor` shows which have keys present.
|
|
|
335
362
|
| `groq` | openai_compat | `openai/gpt-oss-120b` | `GROQ_API_KEY` | free tier: 8k TPM |
|
|
336
363
|
| `anthropic` | anthropic | `claude-sonnet-4-5` | `ANTHROPIC_API_KEY` | |
|
|
337
364
|
| `openai` | openai_compat | `gpt-4.1` | `OPENAI_API_KEY` | |
|
|
338
|
-
| `gemini` | gemini | `gemini-
|
|
365
|
+
| `gemini` | gemini | `gemini-flash-latest` | `GEMINI_API_KEY` | alias, not a pin — see below |
|
|
339
366
|
| `blaxel` | openai_compat | `gpt-4o-mini` | `BL_API_KEY` | sandbox; ignores the model field |
|
|
340
367
|
| `ollama` | openai_compat | `qwen2.5-coder:14b` | none | local |
|
|
341
368
|
|
|
@@ -364,6 +391,15 @@ smoke tests, not for experiments.
|
|
|
364
391
|
NVIDIA model would silently inherit the self-hosted 65k window and $18.16/hr cost
|
|
365
392
|
basis. A regression test guards it.
|
|
366
393
|
|
|
394
|
+
**Gemini's default model is an alias because pinned ones rot in two different ways.**
|
|
395
|
+
0.1.1 shipped `gemini-2.5-pro`, whose free-tier quota is literally zero: a brand-new
|
|
396
|
+
key gets `429 ... generate_content_free_tier_input_token_count, limit: 0` on its first
|
|
397
|
+
request, which reads as a broken adapter rather than a model that was never free.
|
|
398
|
+
Pinning `gemini-2.5-flash` instead fails the other way — `404 no longer available to
|
|
399
|
+
new users`. `gemini-flash-latest` follows Google's own pointer. Worth knowing when
|
|
400
|
+
reading a 429 from any provider: the status code says "slow down", the body says
|
|
401
|
+
whether slowing down will ever help.
|
|
402
|
+
|
|
367
403
|
---
|
|
368
404
|
|
|
369
405
|
## Adding a provider
|
|
@@ -520,11 +556,16 @@ the 12 minutes of loading weights, so the two numbers do not match by design.
|
|
|
520
556
|
## Experiments
|
|
521
557
|
|
|
522
558
|
```bash
|
|
523
|
-
tl experiment run
|
|
524
|
-
tl experiment run
|
|
559
|
+
tl experiment run smoke # free, offline — a shipped config, by name
|
|
560
|
+
tl experiment run bakeoff # real models, real money
|
|
561
|
+
tl experiment run ./my-config.yaml # or your own, by path
|
|
525
562
|
tl experiment report .turnloop/experiments/<run-dir>
|
|
526
563
|
```
|
|
527
564
|
|
|
565
|
+
A bare name resolves against the configs bundled in the package, so the shipped
|
|
566
|
+
experiments are runnable straight from a `pip install`. A local file always wins, so
|
|
567
|
+
your own `smoke.yaml` is never shadowed by ours.
|
|
568
|
+
|
|
528
569
|
Thirteen configs ship, in four families, one runner:
|
|
529
570
|
|
|
530
571
|
- **`bakeoff.yaml`** — same tasks across GLM-5.2, Claude, GPT, Gemini, Groq.
|
|
@@ -792,7 +833,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
|
|
|
792
833
|
## Testing
|
|
793
834
|
|
|
794
835
|
```bash
|
|
795
|
-
pytest #
|
|
836
|
+
pytest # 249 tests, no network
|
|
796
837
|
ruff check turnloop
|
|
797
838
|
mypy turnloop
|
|
798
839
|
```
|
|
@@ -800,14 +841,15 @@ mypy turnloop
|
|
|
800
841
|
| file | tests | |
|
|
801
842
|
|---|---|---|
|
|
802
843
|
| `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
|
|
803
|
-
| `test_experiments.py` |
|
|
804
|
-
| `test_providers.py` |
|
|
805
|
-
| `test_tools_files.py` |
|
|
844
|
+
| `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
|
|
845
|
+
| `test_providers.py` | 31 | adapters against recorded `.sse` fixtures, image block encoding |
|
|
846
|
+
| `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
|
|
806
847
|
| `test_loop.py` | 20 | streaming, truncation, iteration cap |
|
|
807
848
|
| `test_hooks_mcp_commands.py` | 20 | lifecycle hooks, MCP client, slash commands |
|
|
808
849
|
| `test_config.py` | 19 | layering, env overrides, capability presets |
|
|
809
850
|
| `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
|
|
810
851
|
| `test_bash.py` | 14 | shell selection, process-tree kill |
|
|
852
|
+
| `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
|
|
811
853
|
| `test_tui.py` | 8 | Textual snapshots |
|
|
812
854
|
|
|
813
855
|
`conftest.py` monkeypatches httpx's transport to raise unless a test is marked
|
|
@@ -846,7 +888,8 @@ sessions wrongly.
|
|
|
846
888
|
|
|
847
889
|
API keys come from the environment or a `.env` at the project root. A real environment
|
|
848
890
|
variable always wins over the file, and `doctor` reports which key *names* were loaded
|
|
849
|
-
— never their values.
|
|
891
|
+
— never their values. This applies to `search.api_key_env` exactly as it does to a
|
|
892
|
+
provider's: settings files name the variable, never the secret.
|
|
850
893
|
|
|
851
894
|
Project instructions go in `TURNLOOP.md` (`CLAUDE.md` and `AGENTS.md` are also read).
|
|
852
895
|
Slash commands live in `.turnloop/commands/*.md`, skills in
|
|
@@ -864,13 +907,23 @@ just because it is on disk.
|
|
|
864
907
|
|
|
865
908
|
Stated rather than buried:
|
|
866
909
|
|
|
867
|
-
- **
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
-
|
|
871
|
-
- **
|
|
872
|
-
|
|
873
|
-
|
|
910
|
+
- **Image support is verified on two of three encoders.** The `openai_compat` data-URL
|
|
911
|
+
path and Gemini's `inline_data` path both round-trip a real image live; Anthropic's
|
|
912
|
+
is still fixture-only. None of it can be exercised on the primary target, since
|
|
913
|
+
GLM-5.2 W4A16 as deployed is text-only.
|
|
914
|
+
- **No binary file support** beyond those image formats. Null-byte sniff refuses the
|
|
915
|
+
rest. Convert via `Bash`.
|
|
916
|
+
- **`WebSearch`'s default backend parses HTML, not an API.** DuckDuckGo will rate-limit
|
|
917
|
+
and will eventually change its markup; both are handled as distinct, actionable
|
|
918
|
+
errors rather than a crash, but a keyed backend is the durable choice for anything
|
|
919
|
+
that matters. That trade is deliberate: the default has to work with no key at all.
|
|
920
|
+
- **The Anthropic adapter is fixture-tested but has never sent a real request.** It is
|
|
921
|
+
coded against the documented wire format and passes against recorded SSE, which is not
|
|
922
|
+
the same as verified. It is now the only one: the Gemini adapter is verified live
|
|
923
|
+
(text, tool calls and images on `gemini-flash-latest`), and OpenAI needs no separate
|
|
924
|
+
verification because `openai` is `kind="openai_compat"` — the same adapter Groq,
|
|
925
|
+
NVIDIA, Ollama, Blaxel and the self-hosted GLM endpoint all run through, making it
|
|
926
|
+
the most exercised code path in the repo.
|
|
874
927
|
- **`subtle_spec_edge` currently passes for nobody** (0/5 on gpt-4o-mini). The rewrite
|
|
875
928
|
overshot — runs die on collection errors and leave `NotImplementedError`, which is
|
|
876
929
|
failure-to-produce-working-code, not falling for the decoy. It needs another pass.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "turnloop"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "A local-first agentic coding harness: streaming tool loop, permission gating, context compaction, subagents, and any LLM provider."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -35,8 +35,13 @@ dev = [
|
|
|
35
35
|
"pytest>=8.0",
|
|
36
36
|
"pytest-asyncio>=0.23",
|
|
37
37
|
"pytest-textual-snapshot>=1.0",
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# Pinned to a minor range, not floated. Ruff adds lint rules in minor releases,
|
|
39
|
+
# so `>=0.6` means a build that is green today goes red tomorrow with no commit
|
|
40
|
+
# in between — which is how this CI first broke. Bump deliberately.
|
|
41
|
+
"ruff>=0.16.1,<0.17",
|
|
42
|
+
# Must match what is actually run locally, or CI type-checks with a different
|
|
43
|
+
# tool than the author does — which is exactly how this broke.
|
|
44
|
+
"mypy>=2.1,<3",
|
|
40
45
|
]
|
|
41
46
|
|
|
42
47
|
[project.scripts]
|