turnloop 0.1.6__tar.gz → 0.1.8__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 (160) hide show
  1. {turnloop-0.1.6 → turnloop-0.1.8}/PKG-INFO +40 -7
  2. {turnloop-0.1.6 → turnloop-0.1.8}/README.md +39 -6
  3. {turnloop-0.1.6 → turnloop-0.1.8}/pyproject.toml +1 -1
  4. turnloop-0.1.8/tests/test_skills_install.py +523 -0
  5. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_tui.py +30 -0
  6. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/__init__.py +1 -1
  7. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/cli.py +242 -0
  8. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/config.py +29 -0
  9. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/configio.py +16 -1
  10. turnloop-0.1.8/turnloop/skills_install.py +328 -0
  11. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/app.py +32 -0
  12. {turnloop-0.1.6 → turnloop-0.1.8}/.github/workflows/ci.yml +0 -0
  13. {turnloop-0.1.6 → turnloop-0.1.8}/.gitignore +0 -0
  14. {turnloop-0.1.6 → turnloop-0.1.8}/.turnloop/.gitignore +0 -0
  15. {turnloop-0.1.6 → turnloop-0.1.8}/.turnloop/settings.local.json +0 -0
  16. {turnloop-0.1.6 → turnloop-0.1.8}/tests/conftest.py +0 -0
  17. {turnloop-0.1.6 → turnloop-0.1.8}/tests/fixtures/anthropic_tool_call.sse +0 -0
  18. {turnloop-0.1.6 → turnloop-0.1.8}/tests/fixtures/glm_tool_call.sse +0 -0
  19. {turnloop-0.1.6 → turnloop-0.1.8}/tests/fixtures/mcp_echo_server.py +0 -0
  20. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_bash.py +0 -0
  21. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_compaction.py +0 -0
  22. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_config.py +0 -0
  23. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_config_screen.py +0 -0
  24. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_configio.py +0 -0
  25. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_experiments.py +0 -0
  26. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_hooks_mcp_commands.py +0 -0
  27. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_loop.py +0 -0
  28. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_permissions.py +0 -0
  29. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_providers.py +0 -0
  30. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_system_prompt_layout.py +0 -0
  31. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_tools_files.py +0 -0
  32. {turnloop-0.1.6 → turnloop-0.1.8}/tests/test_websearch.py +0 -0
  33. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/__main__.py +0 -0
  34. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/agent/__init__.py +0 -0
  35. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/agent/factory.py +0 -0
  36. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/agent/headless.py +0 -0
  37. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/agent/loop.py +0 -0
  38. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/agent/subagent.py +0 -0
  39. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/agent/system_prompt.py +0 -0
  40. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/commands/__init__.py +0 -0
  41. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/commands/dispatch.py +0 -0
  42. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/commands/loader.py +0 -0
  43. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/context/__init__.py +0 -0
  44. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/context/budget.py +0 -0
  45. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/context/compaction.py +0 -0
  46. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/context/memory.py +0 -0
  47. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/core/__init__.py +0 -0
  48. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/core/events.py +0 -0
  49. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/core/ids.py +0 -0
  50. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/core/messages.py +0 -0
  51. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/core/tokens.py +0 -0
  52. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/diagnostics.py +0 -0
  53. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/errors.py +0 -0
  54. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/__init__.py +0 -0
  55. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/bakeoff.yaml +0 -0
  56. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/ceiling-check.yaml +0 -0
  57. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/context-glm.yaml +0 -0
  58. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/context.yaml +0 -0
  59. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/glm-selfhosted.yaml +0 -0
  60. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/groq-free.yaml +0 -0
  61. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/hard-calibration.yaml +0 -0
  62. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/hard-glm.yaml +0 -0
  63. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/loop.yaml +0 -0
  64. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/nvidia-smoke.yaml +0 -0
  65. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/reliability.yaml +0 -0
  66. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/rewrite-calibration.yaml +0 -0
  67. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/configs/smoke.yaml +0 -0
  68. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/graders.py +0 -0
  69. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/report.py +0 -0
  70. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/runner.py +0 -0
  71. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/default.yaml +0 -0
  72. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_1.py +0 -0
  73. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_2.py +0 -0
  74. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_3.py +0 -0
  75. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_4.py +0 -0
  76. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_5.py +0 -0
  77. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_6.py +0 -0
  78. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_7.py +0 -0
  79. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/bulky/module_8.py +0 -0
  80. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/circular_import/models.py +0 -0
  81. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/circular_import/services.py +0 -0
  82. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +0 -0
  83. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/cli_flag/app.py +0 -0
  84. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +0 -0
  85. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +0 -0
  86. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +0 -0
  87. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +0 -0
  88. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/empty/.keep +0 -0
  89. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/failing_test/calc.py +0 -0
  90. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/failing_test/test_calc.py +0 -0
  91. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/generate_bulky.py +0 -0
  92. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/grep_edit/ingest.py +0 -0
  93. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/grep_edit/load.py +0 -0
  94. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/grep_edit/transform.py +0 -0
  95. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +0 -0
  96. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +0 -0
  97. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +0 -0
  98. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +0 -0
  99. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +0 -0
  100. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/needle/inventory.py +0 -0
  101. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/needle/pricing.py +0 -0
  102. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/needle/shipping.py +0 -0
  103. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +0 -0
  104. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/regression_trap/validators.py +0 -0
  105. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/rename/billing.py +0 -0
  106. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/rename/report.py +0 -0
  107. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/rename/test_billing.py +0 -0
  108. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/spec/parser.py +0 -0
  109. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/spec/test_parser.py +0 -0
  110. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +0 -0
  111. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +0 -0
  112. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/experiments/suite/fixtures/util/util.py +0 -0
  113. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/hooks/__init__.py +0 -0
  114. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/hooks/runner.py +0 -0
  115. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/mcp/__init__.py +0 -0
  116. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/mcp/adapter.py +0 -0
  117. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/mcp/client.py +0 -0
  118. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/permissions/__init__.py +0 -0
  119. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/permissions/engine.py +0 -0
  120. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/permissions/rules.py +0 -0
  121. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/__init__.py +0 -0
  122. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/anthropic.py +0 -0
  123. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/base.py +0 -0
  124. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/gemini.py +0 -0
  125. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/mock.py +0 -0
  126. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/openai_compat.py +0 -0
  127. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/pricing.py +0 -0
  128. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/registry.py +0 -0
  129. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/providers/sse.py +0 -0
  130. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/sessions/__init__.py +0 -0
  131. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/sessions/models.py +0 -0
  132. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/sessions/store.py +0 -0
  133. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/__init__.py +0 -0
  134. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/ask.py +0 -0
  135. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/base.py +0 -0
  136. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/bash.py +0 -0
  137. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/builtin.py +0 -0
  138. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/edit.py +0 -0
  139. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/glob.py +0 -0
  140. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/grep.py +0 -0
  141. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/read.py +0 -0
  142. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/runner.py +0 -0
  143. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/shell.py +0 -0
  144. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/skill.py +0 -0
  145. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/task.py +0 -0
  146. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/textio.py +0 -0
  147. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/todo.py +0 -0
  148. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/webfetch.py +0 -0
  149. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/websearch.py +0 -0
  150. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tools/write.py +0 -0
  151. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/__init__.py +0 -0
  152. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/app.tcss +0 -0
  153. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/bridge.py +0 -0
  154. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/config_screen.py +0 -0
  155. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/mcp_screen.py +0 -0
  156. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/session_screen.py +0 -0
  157. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/widgets/__init__.py +0 -0
  158. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/widgets/permission.py +0 -0
  159. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/widgets/status.py +0 -0
  160. {turnloop-0.1.6 → turnloop-0.1.8}/turnloop/tui/widgets/transcript.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: turnloop
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
- ~20,000 lines of Python, 318 tests, no network in the default test run.
52
+ ~20,000 lines of Python, 346 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.6
120
+ tl --version # turnloop 0.1.8
121
121
  tl doctor # every provider, key presence, shell, context budget
122
122
  ```
123
123
 
@@ -1023,7 +1023,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
1023
1023
  ## Testing
1024
1024
 
1025
1025
  ```bash
1026
- pytest # 318 tests, no network
1026
+ pytest # 346 tests, no network
1027
1027
  ruff check turnloop
1028
1028
  mypy turnloop
1029
1029
  ```
@@ -1035,8 +1035,9 @@ mypy turnloop
1035
1035
  | `test_providers.py` | 35 | adapters vs recorded `.sse`, image blocks, thought signatures |
1036
1036
  | `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
1037
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 |
1038
+ | `test_tui.py` | 25 | Textual snapshots, `/config` and `/mcp add` driven through a real app |
1039
1039
  | `test_config.py` | 23 | layering, env overrides, presets, narrow-console guard |
1040
+ | `test_skills_install.py` | 27 | frontmatter validation, GitHub resolution, path dedupe, EOF on every prompt, drive-root refusal |
1040
1041
  | `test_loop.py` | 23 | streaming, truncation, iteration cap |
1041
1042
  | `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
1042
1043
  | `test_configio.py` | 15 | minimal-diff writes, atomic save, secret-bearing MCP targets, the settings deny rules |
@@ -1107,8 +1108,9 @@ just because it is on disk.
1107
1108
  ### Editing configuration
1108
1109
 
1109
1110
  `tl config --edit` edits the provider table, permission mode, tool verbosity, iteration
1110
- cap, search backend, memory, and the three rule lists. `tl mcp` manages MCP servers. The
1111
- same screens open inside a running session as `/config` and `/mcp add`.
1111
+ cap, search backend, memory, and the three rule lists. `tl mcp` manages MCP servers.
1112
+ `tl skills add/list/remove` manages skills. The config and MCP screens open inside a
1113
+ running session as `/config` and `/mcp add`.
1112
1114
 
1113
1115
  **Adding a provider** is a form for `kind`, `model`, `base_url`, and `api_key_env`, with
1114
1116
  a live ✓/✗ showing whether that environment variable is currently set. Capabilities are
@@ -1185,6 +1187,37 @@ unavailable" and never blocks the loop. Connections are lazy and every call is b
1185
1187
  None of that helps if you install a server that does exactly what it says. Read what you
1186
1188
  add.
1187
1189
 
1190
+ ### Skills are a trust decision too
1191
+
1192
+ `tl skills add <owner/repo>` resolves a GitHub repo shorthand, a repo URL, or a direct
1193
+ raw URL to a `SKILL.md`, and installs it to `.turnloop/skills/<name>/` (or
1194
+ `~/.turnloop/skills/` with `--user`). The same reasoning as the MCP section above
1195
+ applies without modification: a skill's body is not metadata, it is text that gets
1196
+ loaded straight into the model's context the moment the model decides the skill
1197
+ applies (`commands/loader.py`). A hostile `SKILL.md` can instruct the model exactly
1198
+ like a hostile MCP tool description can. `add` prints the source URL and the
1199
+ description and asks for confirmation before writing anything; `--yes` skips waiting
1200
+ for that confirmation but never skips printing it, and never skips printing what got
1201
+ installed and from where.
1202
+
1203
+ Before writing, `add` parses the frontmatter with the same `parse_frontmatter` the
1204
+ loader uses and requires a non-empty `description` — the exact rule `load_skills`
1205
+ enforces when deciding what to advertise (see "A skill that failed to parse vanished
1206
+ with no error" below). A `SKILL.md` that would silently vanish once installed is
1207
+ refused before it is ever written, rather than discovered later as "the skill isn't
1208
+ there."
1209
+
1210
+ **Importing from Claude Code.** If `~/.claude/skills/*/SKILL.md` exist and turnloop
1211
+ has never asked about them, one line appears once, in the TUI, pointing at
1212
+ `tl skills import` — not a prompt that blocks startup, and not a silent copy.
1213
+ Importing computes the token cost of each candidate with `rough_tokens` before you
1214
+ choose (34 skills advertise for about 2,961 tokens of permanent system-prompt
1215
+ overhead — most of a session's `CONFIG_LAYOUT` segment on its own), lets you import
1216
+ any subset, and copies the chosen files into turnloop's own skills directory rather
1217
+ than reading `~/.claude/skills` at runtime — turnloop stays uncoupled from another
1218
+ tool's directory layout. Whether you say yes or no, the question is recorded in the
1219
+ user-level settings file so it is never asked again.
1220
+
1188
1221
  ---
1189
1222
 
1190
1223
  ## Limitations
@@ -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
- ~20,000 lines of Python, 318 tests, no network in the default test run.
23
+ ~20,000 lines of Python, 346 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.6
91
+ tl --version # turnloop 0.1.8
92
92
  tl doctor # every provider, key presence, shell, context budget
93
93
  ```
94
94
 
@@ -994,7 +994,7 @@ one-line edit in a CRLF checkout does not produce a whole-file diff.
994
994
  ## Testing
995
995
 
996
996
  ```bash
997
- pytest # 318 tests, no network
997
+ pytest # 346 tests, no network
998
998
  ruff check turnloop
999
999
  mypy turnloop
1000
1000
  ```
@@ -1006,8 +1006,9 @@ mypy turnloop
1006
1006
  | `test_providers.py` | 35 | adapters vs recorded `.sse`, image blocks, thought signatures |
1007
1007
  | `test_tools_files.py` | 28 | Read/Write/Edit/Glob/Grep, encodings, newlines, image detection |
1008
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 |
1009
+ | `test_tui.py` | 25 | Textual snapshots, `/config` and `/mcp add` driven through a real app |
1010
1010
  | `test_config.py` | 23 | layering, env overrides, presets, narrow-console guard |
1011
+ | `test_skills_install.py` | 27 | frontmatter validation, GitHub resolution, path dedupe, EOF on every prompt, drive-root refusal |
1011
1012
  | `test_loop.py` | 23 | streaming, truncation, iteration cap |
1012
1013
  | `test_compaction.py` | 16 | three tiers, tool_use/tool_result invariant |
1013
1014
  | `test_configio.py` | 15 | minimal-diff writes, atomic save, secret-bearing MCP targets, the settings deny rules |
@@ -1078,8 +1079,9 @@ just because it is on disk.
1078
1079
  ### Editing configuration
1079
1080
 
1080
1081
  `tl config --edit` edits the provider table, permission mode, tool verbosity, iteration
1081
- cap, search backend, memory, and the three rule lists. `tl mcp` manages MCP servers. The
1082
- same screens open inside a running session as `/config` and `/mcp add`.
1082
+ cap, search backend, memory, and the three rule lists. `tl mcp` manages MCP servers.
1083
+ `tl skills add/list/remove` manages skills. The config and MCP screens open inside a
1084
+ running session as `/config` and `/mcp add`.
1083
1085
 
1084
1086
  **Adding a provider** is a form for `kind`, `model`, `base_url`, and `api_key_env`, with
1085
1087
  a live ✓/✗ showing whether that environment variable is currently set. Capabilities are
@@ -1156,6 +1158,37 @@ unavailable" and never blocks the loop. Connections are lazy and every call is b
1156
1158
  None of that helps if you install a server that does exactly what it says. Read what you
1157
1159
  add.
1158
1160
 
1161
+ ### Skills are a trust decision too
1162
+
1163
+ `tl skills add <owner/repo>` resolves a GitHub repo shorthand, a repo URL, or a direct
1164
+ raw URL to a `SKILL.md`, and installs it to `.turnloop/skills/<name>/` (or
1165
+ `~/.turnloop/skills/` with `--user`). The same reasoning as the MCP section above
1166
+ applies without modification: a skill's body is not metadata, it is text that gets
1167
+ loaded straight into the model's context the moment the model decides the skill
1168
+ applies (`commands/loader.py`). A hostile `SKILL.md` can instruct the model exactly
1169
+ like a hostile MCP tool description can. `add` prints the source URL and the
1170
+ description and asks for confirmation before writing anything; `--yes` skips waiting
1171
+ for that confirmation but never skips printing it, and never skips printing what got
1172
+ installed and from where.
1173
+
1174
+ Before writing, `add` parses the frontmatter with the same `parse_frontmatter` the
1175
+ loader uses and requires a non-empty `description` — the exact rule `load_skills`
1176
+ enforces when deciding what to advertise (see "A skill that failed to parse vanished
1177
+ with no error" below). A `SKILL.md` that would silently vanish once installed is
1178
+ refused before it is ever written, rather than discovered later as "the skill isn't
1179
+ there."
1180
+
1181
+ **Importing from Claude Code.** If `~/.claude/skills/*/SKILL.md` exist and turnloop
1182
+ has never asked about them, one line appears once, in the TUI, pointing at
1183
+ `tl skills import` — not a prompt that blocks startup, and not a silent copy.
1184
+ Importing computes the token cost of each candidate with `rough_tokens` before you
1185
+ choose (34 skills advertise for about 2,961 tokens of permanent system-prompt
1186
+ overhead — most of a session's `CONFIG_LAYOUT` segment on its own), lets you import
1187
+ any subset, and copies the chosen files into turnloop's own skills directory rather
1188
+ than reading `~/.claude/skills` at runtime — turnloop stays uncoupled from another
1189
+ tool's directory layout. Whether you say yes or no, the question is recorded in the
1190
+ user-level settings file so it is never asked again.
1191
+
1159
1192
  ---
1160
1193
 
1161
1194
  ## Limitations
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "turnloop"
7
- version = "0.1.6"
7
+ version = "0.1.8"
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"