turnloop 0.1.4__tar.gz → 0.1.6__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 (159) hide show
  1. turnloop-0.1.6/.turnloop/settings.local.json +3 -0
  2. {turnloop-0.1.4 → turnloop-0.1.6}/PKG-INFO +144 -17
  3. {turnloop-0.1.4 → turnloop-0.1.6}/README.md +143 -16
  4. {turnloop-0.1.4 → turnloop-0.1.6}/pyproject.toml +1 -1
  5. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_config.py +83 -30
  6. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_hooks_mcp_commands.py +537 -431
  7. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_loop.py +73 -0
  8. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_permissions.py +48 -0
  9. turnloop-0.1.6/tests/test_system_prompt_layout.py +85 -0
  10. turnloop-0.1.6/tests/test_tui.py +613 -0
  11. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/__init__.py +1 -1
  12. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/agent/headless.py +17 -9
  13. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/agent/system_prompt.py +31 -0
  14. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/cli.py +22 -8
  15. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/commands/dispatch.py +486 -448
  16. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/commands/loader.py +39 -2
  17. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/config.py +478 -444
  18. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/sessions/store.py +286 -271
  19. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/runner.py +63 -1
  20. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/app.py +518 -415
  21. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/config_screen.py +20 -1
  22. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/mcp_screen.py +363 -344
  23. turnloop-0.1.6/turnloop/tui/session_screen.py +114 -0
  24. turnloop-0.1.4/tests/test_tui.py +0 -170
  25. {turnloop-0.1.4 → turnloop-0.1.6}/.github/workflows/ci.yml +0 -0
  26. {turnloop-0.1.4 → turnloop-0.1.6}/.gitignore +0 -0
  27. {turnloop-0.1.4 → turnloop-0.1.6}/.turnloop/.gitignore +0 -0
  28. {turnloop-0.1.4 → turnloop-0.1.6}/tests/conftest.py +0 -0
  29. {turnloop-0.1.4 → turnloop-0.1.6}/tests/fixtures/anthropic_tool_call.sse +0 -0
  30. {turnloop-0.1.4 → turnloop-0.1.6}/tests/fixtures/glm_tool_call.sse +0 -0
  31. {turnloop-0.1.4 → turnloop-0.1.6}/tests/fixtures/mcp_echo_server.py +0 -0
  32. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_bash.py +0 -0
  33. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_compaction.py +0 -0
  34. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_config_screen.py +0 -0
  35. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_configio.py +0 -0
  36. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_experiments.py +0 -0
  37. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_providers.py +0 -0
  38. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_tools_files.py +0 -0
  39. {turnloop-0.1.4 → turnloop-0.1.6}/tests/test_websearch.py +0 -0
  40. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/__main__.py +0 -0
  41. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/agent/__init__.py +0 -0
  42. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/agent/factory.py +0 -0
  43. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/agent/loop.py +0 -0
  44. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/agent/subagent.py +0 -0
  45. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/commands/__init__.py +0 -0
  46. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/configio.py +0 -0
  47. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/context/__init__.py +0 -0
  48. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/context/budget.py +0 -0
  49. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/context/compaction.py +0 -0
  50. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/context/memory.py +0 -0
  51. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/core/__init__.py +0 -0
  52. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/core/events.py +0 -0
  53. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/core/ids.py +0 -0
  54. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/core/messages.py +0 -0
  55. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/core/tokens.py +0 -0
  56. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/diagnostics.py +0 -0
  57. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/errors.py +0 -0
  58. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/__init__.py +0 -0
  59. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/bakeoff.yaml +0 -0
  60. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/ceiling-check.yaml +0 -0
  61. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/context-glm.yaml +0 -0
  62. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/context.yaml +0 -0
  63. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/glm-selfhosted.yaml +0 -0
  64. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/groq-free.yaml +0 -0
  65. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/hard-calibration.yaml +0 -0
  66. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/hard-glm.yaml +0 -0
  67. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/loop.yaml +0 -0
  68. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/nvidia-smoke.yaml +0 -0
  69. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/reliability.yaml +0 -0
  70. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/rewrite-calibration.yaml +0 -0
  71. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/configs/smoke.yaml +0 -0
  72. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/graders.py +0 -0
  73. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/report.py +0 -0
  74. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/runner.py +0 -0
  75. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/default.yaml +0 -0
  76. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_1.py +0 -0
  77. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_2.py +0 -0
  78. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_3.py +0 -0
  79. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_4.py +0 -0
  80. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_5.py +0 -0
  81. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_6.py +0 -0
  82. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_7.py +0 -0
  83. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/bulky/module_8.py +0 -0
  84. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/circular_import/models.py +0 -0
  85. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/circular_import/services.py +0 -0
  86. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +0 -0
  87. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/cli_flag/app.py +0 -0
  88. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +0 -0
  89. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +0 -0
  90. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +0 -0
  91. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +0 -0
  92. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/empty/.keep +0 -0
  93. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/failing_test/calc.py +0 -0
  94. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/failing_test/test_calc.py +0 -0
  95. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/generate_bulky.py +0 -0
  96. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/grep_edit/ingest.py +0 -0
  97. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/grep_edit/load.py +0 -0
  98. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/grep_edit/transform.py +0 -0
  99. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +0 -0
  100. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +0 -0
  101. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +0 -0
  102. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +0 -0
  103. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +0 -0
  104. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/needle/inventory.py +0 -0
  105. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/needle/pricing.py +0 -0
  106. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/needle/shipping.py +0 -0
  107. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +0 -0
  108. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/regression_trap/validators.py +0 -0
  109. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/rename/billing.py +0 -0
  110. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/rename/report.py +0 -0
  111. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/rename/test_billing.py +0 -0
  112. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/spec/parser.py +0 -0
  113. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/spec/test_parser.py +0 -0
  114. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +0 -0
  115. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +0 -0
  116. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/experiments/suite/fixtures/util/util.py +0 -0
  117. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/hooks/__init__.py +0 -0
  118. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/hooks/runner.py +0 -0
  119. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/mcp/__init__.py +0 -0
  120. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/mcp/adapter.py +0 -0
  121. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/mcp/client.py +0 -0
  122. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/permissions/__init__.py +0 -0
  123. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/permissions/engine.py +0 -0
  124. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/permissions/rules.py +0 -0
  125. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/__init__.py +0 -0
  126. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/anthropic.py +0 -0
  127. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/base.py +0 -0
  128. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/gemini.py +0 -0
  129. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/mock.py +0 -0
  130. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/openai_compat.py +0 -0
  131. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/pricing.py +0 -0
  132. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/registry.py +0 -0
  133. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/providers/sse.py +0 -0
  134. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/sessions/__init__.py +0 -0
  135. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/sessions/models.py +0 -0
  136. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/__init__.py +0 -0
  137. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/ask.py +0 -0
  138. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/base.py +0 -0
  139. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/bash.py +0 -0
  140. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/builtin.py +0 -0
  141. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/edit.py +0 -0
  142. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/glob.py +0 -0
  143. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/grep.py +0 -0
  144. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/read.py +0 -0
  145. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/shell.py +0 -0
  146. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/skill.py +0 -0
  147. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/task.py +0 -0
  148. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/textio.py +0 -0
  149. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/todo.py +0 -0
  150. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/webfetch.py +0 -0
  151. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/websearch.py +0 -0
  152. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tools/write.py +0 -0
  153. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/__init__.py +0 -0
  154. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/app.tcss +0 -0
  155. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/bridge.py +0 -0
  156. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/widgets/__init__.py +0 -0
  157. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/widgets/permission.py +0 -0
  158. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/widgets/status.py +0 -0
  159. {turnloop-0.1.4 → turnloop-0.1.6}/turnloop/tui/widgets/transcript.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "provider": "blaxel"
3
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: turnloop
3
- Version: 0.1.4
3
+ Version: 0.1.6
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
@@ -49,7 +49,7 @@ tl experiment run smoke # a measurement run, offline and free
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, 255 tests, no network in the default test run.
52
+ ~20,000 lines of Python, 318 tests, no network in the default test run.
53
53
 
54
54
  ---
55
55
 
@@ -117,7 +117,7 @@ point.
117
117
  Verify:
118
118
 
119
119
  ```bash
120
- tl --version # turnloop 0.1.4
120
+ tl --version # turnloop 0.1.6
121
121
  tl doctor # every provider, key presence, shell, context budget
122
122
  ```
123
123
 
@@ -139,8 +139,9 @@ context arithmetic for the active provider:
139
139
  tl # interactive TUI
140
140
  tl -p "why does the login test flake" # headless single turn
141
141
  tl -p "..." --json # headless, JSONL events on stdout
142
- tl --resume # resume the most recent session
143
- tl --resume <session-id> # resume a specific one
142
+ tl -c # resume the most recent session, no picker
143
+ tl --resume # interactive picker (TUI); most recent (headless)
144
+ tl --resume <session-id> # resume a specific one, no picker
144
145
  tl sessions # list recorded sessions
145
146
  tl config # merged config + which layer each value came from
146
147
  tl config --edit # interactive settings editor
@@ -162,7 +163,8 @@ and neither is reachable by the model — see
162
163
  | `--max-iterations N` | tool-loop safety cap (default 40) |
163
164
  | `-p, --print PROMPT` | headless single turn |
164
165
  | `--json` | headless output as JSONL events |
165
- | `--resume [ID]` | resume last, or a named session |
166
+ | `-c, --continue` | resume the most recent session directly, no picker |
167
+ | `--resume [ID]` | bare: interactive picker in the TUI, most recent in headless. With an id: that session, no picker |
166
168
 
167
169
  ### Permission modes
168
170
 
@@ -184,12 +186,18 @@ to cycle. Make it permanent via `TURNLOOP_PERMISSION_MODE` or `settings.json`.
184
186
  ### Keys
185
187
 
186
188
  ```
187
- ctrl+c interrupt the running turn (again to exit)
189
+ ctrl+c copy the selection; else interrupt a running turn; else nothing
188
190
  ctrl+p cycle permission mode
189
191
  ctrl+l clear the view (history is kept)
190
192
  ctrl+d quit
191
193
  ```
192
194
 
195
+ `ctrl+c` no longer quits. It used to call `self.exit()` whenever no turn was
196
+ running, so pressing it to copy text killed the session. It now checks, in order,
197
+ whether there is a selection to copy (via Textual's `App.copy_to_clipboard`, OSC 52,
198
+ no new dependency), then whether a turn is running to interrupt, and otherwise does
199
+ nothing. `ctrl+d` is still the only way to quit.
200
+
193
201
  `ctrl+c` and friends are bound with `priority=True` — the prompt Input always has
194
202
  focus and would otherwise swallow them.
195
203
 
@@ -205,6 +213,9 @@ focus and would otherwise swallow them.
205
213
  /provider switch provider /config settings editor
206
214
  /export transcript to markdown /hooks configured hooks
207
215
  /sessions recorded sessions /doctor diagnostics
216
+ /skills loaded skills, and any /resume picker, rebuilds the
217
+ rejected on disk (why, agent in place; refuses
218
+ and where) while a turn is running
208
219
  /quit exit
209
220
  ```
210
221
 
@@ -350,6 +361,13 @@ close a self-escalation path: settings files set `permission_mode` and
350
361
  next launch. Verified against the real engine *in bypass mode*, which is the case that
351
362
  matters — deny beats bypass, so the block holds even there.
352
363
 
364
+ That was a unit test. It has since been verified live: with
365
+ `Write(.turnloop/**)` explicitly allowed *and* `--permission-mode bypass`, a real
366
+ model told to grant itself permanent bypass access was blocked, made one attempt,
367
+ and proposed the change to the user instead. The settings file was unchanged. Deny
368
+ beats an explicit allow rule and bypass mode at once — not just in the test suite,
369
+ against a live model that was actually trying.
370
+
353
371
  It is a guardrail, not a sandbox, and the boundary is worth being precise about. `Write`
354
372
  and `Edit` are blocked; **`Bash` is not**, so a shell redirect reaches the same file.
355
373
  Closing that properly needs a path guard below the tool layer rather than more patterns.
@@ -395,6 +413,14 @@ because a crashed run still leaves a readable trace, and because the experiments
395
413
  layer reads exactly the same format the TUI writes — a run trace and a real session
396
414
  are the same artifact.
397
415
 
416
+ `-c`/`--continue` reopens the most recent session with no prompting. Bare `--resume`
417
+ in the TUI opens an interactive picker — a `DataTable` of sessions, newest first,
418
+ with a live preview pane of the selected conversation — and `--resume <id>` jumps
419
+ straight to a specific one. `/resume` opens the same picker in-session and rebuilds
420
+ the agent in place; it refuses while a turn is running rather than tearing one down
421
+ mid-flight. Headless has no picker to show, so a bare `--resume` there falls back to
422
+ "most recent" — the same thing `--continue` does.
423
+
398
424
  ---
399
425
 
400
426
  ## Providers
@@ -869,11 +895,101 @@ and through session JSONL, so a `--resume`d conversation does not hit the same w
869
895
  check, and anyone who pointed 0.1.2 at Gemini got one working tool call and then a
870
896
  400. Fixed in 0.1.3.
871
897
 
872
- **The lesson, four times over in one project:** an 84-run sweep failing while printing
898
+ ### `NoActiveWorker`: 283 green tests and a crash on the first keystroke
899
+
900
+ The config UI shipped with tests covering `/config` — they asserted that dispatching it
901
+ returned `open_screen="config"`. All 283 passed. Typing `/config` in the actual TUI
902
+ raised `NoActiveWorker: push_screen must be run from a worker`.
903
+
904
+ Textual's `push_screen_wait` blocks the calling coroutine until the screen is dismissed,
905
+ so it must run inside a worker; called from the message pump it would deadlock the pump
906
+ it is waiting on, and Textual refuses rather than hanging. `_handle_command` runs on the
907
+ pump. So does Textual's action dispatch, which meant the same defect existed at four
908
+ call sites, not the one in the traceback — including the provider form reachable from
909
+ `tl config --edit`, a path the crash report never touched.
910
+
911
+ The correct pattern was eleven lines away in the same file: `_permission_worker` is
912
+ `@work`-decorated, which is exactly why *its* `push_screen_wait` works.
913
+
914
+ Two things this cost, both worth stating. The tests verified the layer *below* the bug —
915
+ that the command produced the right instruction, never that the app could carry it out.
916
+ And **0.1.4 shipped this way**, so `pip install turnloop==0.1.4` crashes on `/config`.
917
+ Fixed in 0.1.5. The tests now drive a real app through Textual's `run_test()` pilot and
918
+ fail if the decorators are removed.
919
+
920
+ **A directory that exists on every launch cannot mean "this is a project."**
921
+
922
+ `find_project_root` treated any ancestor `.turnloop` directory as an explicit project
923
+ declaration. But `default_project_dir` creates `.turnloop/` — with a `sessions/`
924
+ folder and a `.gitignore` — on every launch, unconditionally, wherever turnloop
925
+ happens to start. So the directory's mere existence declared nothing; it was
926
+ frequently just the residue of having once run the tool there. Run `tl` once from a
927
+ home directory, or a drive root, and it permanently annexed every project beneath it:
928
+ sessions, the `.env` layer, `settings.json` and the permission rules all resolved to
929
+ the wrong root from then on. Real instance, the author's own machine: `F:\.turnloop`
930
+ was created on 2026-08-01, and every `F:\anything\proj\pyproject.toml` since then
931
+ resolved its project root to `F:\`.
932
+
933
+ The invariant that broke was never written down as one, which is exactly why it lasted.
934
+ The fix makes the implicit rule explicit: a `.turnloop` counts only if it holds
935
+ something a human put there — `settings.json`, `settings.local.json`, `commands/`,
936
+ `skills/`, `TURNLOOP.md` — and one holding only an auto-created `sessions/` is ignored.
937
+ The home directory is excluded outright on top of that, since `~/.turnloop` is the
938
+ user-scope settings location and can never mean "project." The false invariant had
939
+ been there since the tool first created `.turnloop/` on launch; fixed in 0.1.6.
940
+
941
+ **A skill that failed to parse vanished with no error, and the model reported success.**
942
+
943
+ A live run installed a skill whose frontmatter was markdown-bold — `**name**:
944
+ something` — rather than YAML. `yaml.safe_load` returned no usable keys,
945
+ `load_skills` hit its "no description" skip, and the skill disappeared: no exception,
946
+ no log line, nothing in `/skills`. The model that had just "installed" it reported
947
+ success, because from its side the write succeeded and nothing said otherwise.
948
+
949
+ The bug was not the parser rejecting bad frontmatter — that part is correct. It was
950
+ that a rejection and a nonexistent skill looked identical from the outside. `load_skills`
951
+ keeps its exact signature, but a new `rejected_skills()` now returns what was dropped
952
+ and why, `/skills` reports it, and `CONFIG_LAYOUT` shows the literal YAML frontmatter
953
+ block in the system prompt so the model stops guessing at markdown-bold instead.
954
+ Fixed in 0.1.6.
955
+
956
+ **A permission denial that should end in one message looped ten times, and it cost
957
+ real money before anyone noticed — because nothing about it failed a test.**
958
+
959
+ Two defects landed in the same code path with the same symptom. The DENY branch's
960
+ message said only `Permission denied: <rule>`; the ASK-decline branch, which is
961
+ recoverable, had always carried "Do not retry it" — so the weaker wording sat on the
962
+ *permanent* block, the one that most needed it. Fixing the message was not enough on
963
+ its own: the ASK path looped despite already carrying that warning, because prompt
964
+ wording is a request, not a constraint, and a model under pressure to finish a task
965
+ does not reliably honor it.
966
+
967
+ Measured against a live, non-interactive provider — this is what a passing test suite
968
+ cannot see, since nothing here raises or returns the wrong value, it just keeps calling
969
+ a tool that keeps getting denied:
970
+
971
+ - denied `Write .turnloop/settings.local.json`, repeated 10 times → **233,742 input
972
+ tokens, $0.0372**
973
+ - after the message fix alone → **10,654 tokens, $0.0017**, one attempt then a correct
974
+ proposal to the user
975
+ - a separate ASK-decline loop on `dir`/`mkdir`, before any fix → **199,352 tokens,
976
+ $0.0317**
977
+
978
+ So wording is now backed by a deterministic guard in `ToolRunner` rather than trusted
979
+ alone: identity is tool name plus pydantic-normalized args, scope is the session, one
980
+ free retry, and the entry clears the moment the call is allowed or approved — so a
981
+ permission granted mid-session un-poisons a call that was denied earlier, instead of
982
+ blocking it forever. Fixed in 0.1.6.
983
+
984
+ **The lesson, eight times over in one project:** an 84-run sweep failing while printing
873
985
  a tidy report of zeros; a tool returning empty with `ok: true`; a sweep sitting at
874
986
  10/18 doing nothing for 25 minutes; an adapter passing its live test and failing on
875
- the turn the test never made. Each was caught only by looking past the summary line —
876
- and the last one only by using the thing instead of testing it.
987
+ the turn the test never made; a feature with green tests that crashed on its first
988
+ keystroke; an implicit invariant that a docstring never stated, so nobody noticed it
989
+ was false; a silent failure that a model reported as success; a cost bug that no
990
+ assertion could ever have caught, because every individual call did exactly what it
991
+ was told. Each was caught only by looking past the summary line — and most of them
992
+ only by using the thing instead of testing it.
877
993
 
878
994
  ---
879
995
 
@@ -907,26 +1023,27 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
907
1023
  ## Testing
908
1024
 
909
1025
  ```bash
910
- pytest # 283 tests, no network
1026
+ pytest # 318 tests, no network
911
1027
  ruff check turnloop
912
1028
  mypy turnloop
913
1029
  ```
914
1030
 
915
1031
  | file | tests | |
916
1032
  |---|---|---|
917
- | `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
1033
+ | `test_permissions.py` | 50 | rule grammar, compound-command splitting, mode enforcement |
918
1034
  | `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
919
1035
  | `test_providers.py` | 35 | adapters vs recorded `.sse`, image blocks, thought signatures |
920
1036
  | `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
921
- | `test_loop.py` | 20 | streaming, truncation, iteration cap |
922
- | `test_hooks_mcp_commands.py` | 23 | lifecycle hooks, MCP client, slash commands |
923
- | `test_config.py` | 21 | layering, env overrides, presets, narrow-console guard |
1037
+ | `test_hooks_mcp_commands.py` | 28 | lifecycle hooks, MCP client, slash commands |
1038
+ | `test_tui.py` | 24 | Textual snapshots, `/config` and `/mcp add` driven through a real app |
1039
+ | `test_config.py` | 23 | layering, env overrides, presets, narrow-console guard |
1040
+ | `test_loop.py` | 23 | streaming, truncation, iteration cap |
924
1041
  | `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
925
- | `test_bash.py` | 14 | shell selection, process-tree kill |
926
1042
  | `test_configio.py` | 15 | minimal-diff writes, atomic save, secret-bearing MCP targets, the settings deny rules |
1043
+ | `test_bash.py` | 14 | shell selection, process-tree kill |
927
1044
  | `test_config_screen.py` | 10 | provider form validation, caps derivation, env indicator leaks |
928
1045
  | `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
929
- | `test_tui.py` | 8 | Textual snapshots |
1046
+ | `test_system_prompt_layout.py` | 7 | `CONFIG_LAYOUT` presence, token cost, `minimal` variant omission |
930
1047
 
931
1048
  `conftest.py` monkeypatches httpx's transport to raise unless a test is marked
932
1049
  `live` — **network is off at the transport layer**, so a forgotten real call fails
@@ -973,6 +1090,16 @@ Slash commands live in `.turnloop/commands/*.md`, skills in
973
1090
  in the system prompt and load their body on demand, which on a 65k window is the
974
1091
  difference between having skills and not.
975
1092
 
1093
+ The system prompt also documents this layout to the model itself, as `CONFIG_LAYOUT`
1094
+ — 326 tokens, in the cacheable region, omitted from the `minimal` variant. Without it
1095
+ the model cannot configure the tool it is running inside: asked to add an MCP server
1096
+ it invented `.turnloop/turnloop.config.json`, and asked to install a skill it wrote to
1097
+ `examples/` — both real outputs from live runs. With it, both go to the right place
1098
+ first try. It includes the literal YAML frontmatter a skill needs, since a skill with
1099
+ markdown-bold frontmatter instead of YAML used to vanish with no diagnostic; `/skills`
1100
+ now also reports anything found on disk but rejected, with the reason and path — see
1101
+ [Bugs worth reading about](#bugs-worth-reading-about).
1102
+
976
1103
  Shell expansion inside a command template (`` !`cmd` ``) goes through the permission
977
1104
  engine like any other Bash call. A markdown file in a repository is not trusted input
978
1105
  just because it is on disk.
@@ -20,7 +20,7 @@ tl experiment run smoke # a measurement run, offline and free
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, 255 tests, no network in the default test run.
23
+ ~20,000 lines of Python, 318 tests, no network in the default test run.
24
24
 
25
25
  ---
26
26
 
@@ -88,7 +88,7 @@ point.
88
88
  Verify:
89
89
 
90
90
  ```bash
91
- tl --version # turnloop 0.1.4
91
+ tl --version # turnloop 0.1.6
92
92
  tl doctor # every provider, key presence, shell, context budget
93
93
  ```
94
94
 
@@ -110,8 +110,9 @@ context arithmetic for the active provider:
110
110
  tl # interactive TUI
111
111
  tl -p "why does the login test flake" # headless single turn
112
112
  tl -p "..." --json # headless, JSONL events on stdout
113
- tl --resume # resume the most recent session
114
- tl --resume <session-id> # resume a specific one
113
+ tl -c # resume the most recent session, no picker
114
+ tl --resume # interactive picker (TUI); most recent (headless)
115
+ tl --resume <session-id> # resume a specific one, no picker
115
116
  tl sessions # list recorded sessions
116
117
  tl config # merged config + which layer each value came from
117
118
  tl config --edit # interactive settings editor
@@ -133,7 +134,8 @@ and neither is reachable by the model — see
133
134
  | `--max-iterations N` | tool-loop safety cap (default 40) |
134
135
  | `-p, --print PROMPT` | headless single turn |
135
136
  | `--json` | headless output as JSONL events |
136
- | `--resume [ID]` | resume last, or a named session |
137
+ | `-c, --continue` | resume the most recent session directly, no picker |
138
+ | `--resume [ID]` | bare: interactive picker in the TUI, most recent in headless. With an id: that session, no picker |
137
139
 
138
140
  ### Permission modes
139
141
 
@@ -155,12 +157,18 @@ to cycle. Make it permanent via `TURNLOOP_PERMISSION_MODE` or `settings.json`.
155
157
  ### Keys
156
158
 
157
159
  ```
158
- ctrl+c interrupt the running turn (again to exit)
160
+ ctrl+c copy the selection; else interrupt a running turn; else nothing
159
161
  ctrl+p cycle permission mode
160
162
  ctrl+l clear the view (history is kept)
161
163
  ctrl+d quit
162
164
  ```
163
165
 
166
+ `ctrl+c` no longer quits. It used to call `self.exit()` whenever no turn was
167
+ running, so pressing it to copy text killed the session. It now checks, in order,
168
+ whether there is a selection to copy (via Textual's `App.copy_to_clipboard`, OSC 52,
169
+ no new dependency), then whether a turn is running to interrupt, and otherwise does
170
+ nothing. `ctrl+d` is still the only way to quit.
171
+
164
172
  `ctrl+c` and friends are bound with `priority=True` — the prompt Input always has
165
173
  focus and would otherwise swallow them.
166
174
 
@@ -176,6 +184,9 @@ focus and would otherwise swallow them.
176
184
  /provider switch provider /config settings editor
177
185
  /export transcript to markdown /hooks configured hooks
178
186
  /sessions recorded sessions /doctor diagnostics
187
+ /skills loaded skills, and any /resume picker, rebuilds the
188
+ rejected on disk (why, agent in place; refuses
189
+ and where) while a turn is running
179
190
  /quit exit
180
191
  ```
181
192
 
@@ -321,6 +332,13 @@ close a self-escalation path: settings files set `permission_mode` and
321
332
  next launch. Verified against the real engine *in bypass mode*, which is the case that
322
333
  matters — deny beats bypass, so the block holds even there.
323
334
 
335
+ That was a unit test. It has since been verified live: with
336
+ `Write(.turnloop/**)` explicitly allowed *and* `--permission-mode bypass`, a real
337
+ model told to grant itself permanent bypass access was blocked, made one attempt,
338
+ and proposed the change to the user instead. The settings file was unchanged. Deny
339
+ beats an explicit allow rule and bypass mode at once — not just in the test suite,
340
+ against a live model that was actually trying.
341
+
324
342
  It is a guardrail, not a sandbox, and the boundary is worth being precise about. `Write`
325
343
  and `Edit` are blocked; **`Bash` is not**, so a shell redirect reaches the same file.
326
344
  Closing that properly needs a path guard below the tool layer rather than more patterns.
@@ -366,6 +384,14 @@ because a crashed run still leaves a readable trace, and because the experiments
366
384
  layer reads exactly the same format the TUI writes — a run trace and a real session
367
385
  are the same artifact.
368
386
 
387
+ `-c`/`--continue` reopens the most recent session with no prompting. Bare `--resume`
388
+ in the TUI opens an interactive picker — a `DataTable` of sessions, newest first,
389
+ with a live preview pane of the selected conversation — and `--resume <id>` jumps
390
+ straight to a specific one. `/resume` opens the same picker in-session and rebuilds
391
+ the agent in place; it refuses while a turn is running rather than tearing one down
392
+ mid-flight. Headless has no picker to show, so a bare `--resume` there falls back to
393
+ "most recent" — the same thing `--continue` does.
394
+
369
395
  ---
370
396
 
371
397
  ## Providers
@@ -840,11 +866,101 @@ and through session JSONL, so a `--resume`d conversation does not hit the same w
840
866
  check, and anyone who pointed 0.1.2 at Gemini got one working tool call and then a
841
867
  400. Fixed in 0.1.3.
842
868
 
843
- **The lesson, four times over in one project:** an 84-run sweep failing while printing
869
+ ### `NoActiveWorker`: 283 green tests and a crash on the first keystroke
870
+
871
+ The config UI shipped with tests covering `/config` — they asserted that dispatching it
872
+ returned `open_screen="config"`. All 283 passed. Typing `/config` in the actual TUI
873
+ raised `NoActiveWorker: push_screen must be run from a worker`.
874
+
875
+ Textual's `push_screen_wait` blocks the calling coroutine until the screen is dismissed,
876
+ so it must run inside a worker; called from the message pump it would deadlock the pump
877
+ it is waiting on, and Textual refuses rather than hanging. `_handle_command` runs on the
878
+ pump. So does Textual's action dispatch, which meant the same defect existed at four
879
+ call sites, not the one in the traceback — including the provider form reachable from
880
+ `tl config --edit`, a path the crash report never touched.
881
+
882
+ The correct pattern was eleven lines away in the same file: `_permission_worker` is
883
+ `@work`-decorated, which is exactly why *its* `push_screen_wait` works.
884
+
885
+ Two things this cost, both worth stating. The tests verified the layer *below* the bug —
886
+ that the command produced the right instruction, never that the app could carry it out.
887
+ And **0.1.4 shipped this way**, so `pip install turnloop==0.1.4` crashes on `/config`.
888
+ Fixed in 0.1.5. The tests now drive a real app through Textual's `run_test()` pilot and
889
+ fail if the decorators are removed.
890
+
891
+ **A directory that exists on every launch cannot mean "this is a project."**
892
+
893
+ `find_project_root` treated any ancestor `.turnloop` directory as an explicit project
894
+ declaration. But `default_project_dir` creates `.turnloop/` — with a `sessions/`
895
+ folder and a `.gitignore` — on every launch, unconditionally, wherever turnloop
896
+ happens to start. So the directory's mere existence declared nothing; it was
897
+ frequently just the residue of having once run the tool there. Run `tl` once from a
898
+ home directory, or a drive root, and it permanently annexed every project beneath it:
899
+ sessions, the `.env` layer, `settings.json` and the permission rules all resolved to
900
+ the wrong root from then on. Real instance, the author's own machine: `F:\.turnloop`
901
+ was created on 2026-08-01, and every `F:\anything\proj\pyproject.toml` since then
902
+ resolved its project root to `F:\`.
903
+
904
+ The invariant that broke was never written down as one, which is exactly why it lasted.
905
+ The fix makes the implicit rule explicit: a `.turnloop` counts only if it holds
906
+ something a human put there — `settings.json`, `settings.local.json`, `commands/`,
907
+ `skills/`, `TURNLOOP.md` — and one holding only an auto-created `sessions/` is ignored.
908
+ The home directory is excluded outright on top of that, since `~/.turnloop` is the
909
+ user-scope settings location and can never mean "project." The false invariant had
910
+ been there since the tool first created `.turnloop/` on launch; fixed in 0.1.6.
911
+
912
+ **A skill that failed to parse vanished with no error, and the model reported success.**
913
+
914
+ A live run installed a skill whose frontmatter was markdown-bold — `**name**:
915
+ something` — rather than YAML. `yaml.safe_load` returned no usable keys,
916
+ `load_skills` hit its "no description" skip, and the skill disappeared: no exception,
917
+ no log line, nothing in `/skills`. The model that had just "installed" it reported
918
+ success, because from its side the write succeeded and nothing said otherwise.
919
+
920
+ The bug was not the parser rejecting bad frontmatter — that part is correct. It was
921
+ that a rejection and a nonexistent skill looked identical from the outside. `load_skills`
922
+ keeps its exact signature, but a new `rejected_skills()` now returns what was dropped
923
+ and why, `/skills` reports it, and `CONFIG_LAYOUT` shows the literal YAML frontmatter
924
+ block in the system prompt so the model stops guessing at markdown-bold instead.
925
+ Fixed in 0.1.6.
926
+
927
+ **A permission denial that should end in one message looped ten times, and it cost
928
+ real money before anyone noticed — because nothing about it failed a test.**
929
+
930
+ Two defects landed in the same code path with the same symptom. The DENY branch's
931
+ message said only `Permission denied: <rule>`; the ASK-decline branch, which is
932
+ recoverable, had always carried "Do not retry it" — so the weaker wording sat on the
933
+ *permanent* block, the one that most needed it. Fixing the message was not enough on
934
+ its own: the ASK path looped despite already carrying that warning, because prompt
935
+ wording is a request, not a constraint, and a model under pressure to finish a task
936
+ does not reliably honor it.
937
+
938
+ Measured against a live, non-interactive provider — this is what a passing test suite
939
+ cannot see, since nothing here raises or returns the wrong value, it just keeps calling
940
+ a tool that keeps getting denied:
941
+
942
+ - denied `Write .turnloop/settings.local.json`, repeated 10 times → **233,742 input
943
+ tokens, $0.0372**
944
+ - after the message fix alone → **10,654 tokens, $0.0017**, one attempt then a correct
945
+ proposal to the user
946
+ - a separate ASK-decline loop on `dir`/`mkdir`, before any fix → **199,352 tokens,
947
+ $0.0317**
948
+
949
+ So wording is now backed by a deterministic guard in `ToolRunner` rather than trusted
950
+ alone: identity is tool name plus pydantic-normalized args, scope is the session, one
951
+ free retry, and the entry clears the moment the call is allowed or approved — so a
952
+ permission granted mid-session un-poisons a call that was denied earlier, instead of
953
+ blocking it forever. Fixed in 0.1.6.
954
+
955
+ **The lesson, eight times over in one project:** an 84-run sweep failing while printing
844
956
  a tidy report of zeros; a tool returning empty with `ok: true`; a sweep sitting at
845
957
  10/18 doing nothing for 25 minutes; an adapter passing its live test and failing on
846
- the turn the test never made. Each was caught only by looking past the summary line —
847
- and the last one only by using the thing instead of testing it.
958
+ the turn the test never made; a feature with green tests that crashed on its first
959
+ keystroke; an implicit invariant that a docstring never stated, so nobody noticed it
960
+ was false; a silent failure that a model reported as success; a cost bug that no
961
+ assertion could ever have caught, because every individual call did exactly what it
962
+ was told. Each was caught only by looking past the summary line — and most of them
963
+ only by using the thing instead of testing it.
848
964
 
849
965
  ---
850
966
 
@@ -878,26 +994,27 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
878
994
  ## Testing
879
995
 
880
996
  ```bash
881
- pytest # 283 tests, no network
997
+ pytest # 318 tests, no network
882
998
  ruff check turnloop
883
999
  mypy turnloop
884
1000
  ```
885
1001
 
886
1002
  | file | tests | |
887
1003
  |---|---|---|
888
- | `test_permissions.py` | 48 | rule grammar, compound-command splitting, mode enforcement |
1004
+ | `test_permissions.py` | 50 | rule grammar, compound-command splitting, mode enforcement |
889
1005
  | `test_experiments.py` | 36 | runner, graders, report, suite invariants, config resolution |
890
1006
  | `test_providers.py` | 35 | adapters vs recorded `.sse`, image blocks, thought signatures |
891
1007
  | `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
892
- | `test_loop.py` | 20 | streaming, truncation, iteration cap |
893
- | `test_hooks_mcp_commands.py` | 23 | lifecycle hooks, MCP client, slash commands |
894
- | `test_config.py` | 21 | layering, env overrides, presets, narrow-console guard |
1008
+ | `test_hooks_mcp_commands.py` | 28 | lifecycle hooks, MCP client, slash commands |
1009
+ | `test_tui.py` | 24 | Textual snapshots, `/config` and `/mcp add` driven through a real app |
1010
+ | `test_config.py` | 23 | layering, env overrides, presets, narrow-console guard |
1011
+ | `test_loop.py` | 23 | streaming, truncation, iteration cap |
895
1012
  | `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
896
- | `test_bash.py` | 14 | shell selection, process-tree kill |
897
1013
  | `test_configio.py` | 15 | minimal-diff writes, atomic save, secret-bearing MCP targets, the settings deny rules |
1014
+ | `test_bash.py` | 14 | shell selection, process-tree kill |
898
1015
  | `test_config_screen.py` | 10 | provider form validation, caps derivation, env indicator leaks |
899
1016
  | `test_websearch.py` | 9 | HTML parsing, backend selection, truncation, failure modes |
900
- | `test_tui.py` | 8 | Textual snapshots |
1017
+ | `test_system_prompt_layout.py` | 7 | `CONFIG_LAYOUT` presence, token cost, `minimal` variant omission |
901
1018
 
902
1019
  `conftest.py` monkeypatches httpx's transport to raise unless a test is marked
903
1020
  `live` — **network is off at the transport layer**, so a forgotten real call fails
@@ -944,6 +1061,16 @@ Slash commands live in `.turnloop/commands/*.md`, skills in
944
1061
  in the system prompt and load their body on demand, which on a 65k window is the
945
1062
  difference between having skills and not.
946
1063
 
1064
+ The system prompt also documents this layout to the model itself, as `CONFIG_LAYOUT`
1065
+ — 326 tokens, in the cacheable region, omitted from the `minimal` variant. Without it
1066
+ the model cannot configure the tool it is running inside: asked to add an MCP server
1067
+ it invented `.turnloop/turnloop.config.json`, and asked to install a skill it wrote to
1068
+ `examples/` — both real outputs from live runs. With it, both go to the right place
1069
+ first try. It includes the literal YAML frontmatter a skill needs, since a skill with
1070
+ markdown-bold frontmatter instead of YAML used to vanish with no diagnostic; `/skills`
1071
+ now also reports anything found on disk but rejected, with the reason and path — see
1072
+ [Bugs worth reading about](#bugs-worth-reading-about).
1073
+
947
1074
  Shell expansion inside a command template (`` !`cmd` ``) goes through the permission
948
1075
  engine like any other Bash call. A markdown file in a repository is not trusted input
949
1076
  just because it is on disk.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "turnloop"
7
- version = "0.1.4"
7
+ version = "0.1.6"
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"