tether-sdk 0.3.0__tar.gz → 0.3.1__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 (380) hide show
  1. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/AGENTS.md +4 -3
  2. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/PKG-INFO +1 -1
  3. tether_sdk-0.3.1/docs/guides/small-models.md +88 -0
  4. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/index.md +1 -0
  5. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/llms.txt +1 -0
  6. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/providers.md +1 -0
  7. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/release-notes.md +23 -0
  8. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/built-in-tools.md +7 -1
  9. tether_sdk-0.3.1/e2e/test_bash_background.py +15 -0
  10. tether_sdk-0.3.1/e2e/test_broken_python.py +30 -0
  11. tether_sdk-0.3.1/e2e/test_edit_insert.py +14 -0
  12. tether_sdk-0.3.1/e2e/test_edit_shapes.py +15 -0
  13. tether_sdk-0.3.1/e2e/test_last_step.py +19 -0
  14. tether_sdk-0.3.1/e2e/test_provider_limit.py +39 -0
  15. tether_sdk-0.3.1/e2e/test_refused_not_a_change.py +16 -0
  16. tether_sdk-0.3.1/e2e/test_sed_quoted_path.py +14 -0
  17. tether_sdk-0.3.1/e2e/test_web_fetch_anchor.py +18 -0
  18. tether_sdk-0.3.1/e2e/test_worker_pythonpath_e2e.py +31 -0
  19. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/mkdocs.yml +1 -0
  20. tether_sdk-0.3.1/tests/test_broken_python.py +181 -0
  21. tether_sdk-0.3.1/tests/test_copilot_nudges.py +142 -0
  22. tether_sdk-0.3.1/tests/test_edit_shapes.py +110 -0
  23. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_harness.py +24 -2
  24. tether_sdk-0.3.1/tests/test_provider_limit.py +262 -0
  25. tether_sdk-0.3.1/tests/test_sandbox_mask_sockets.py +90 -0
  26. tether_sdk-0.3.1/tests/test_sed_quirk.py +53 -0
  27. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_tether.py +1 -1
  28. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worker_isolation.py +75 -0
  29. tether_sdk-0.3.1/tests/tools/test_bash_background.py +95 -0
  30. tether_sdk-0.3.1/tests/tools/test_edit_insert.py +96 -0
  31. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_web.py +2 -2
  32. tether_sdk-0.3.1/tests/tools/test_web_copilot.py +143 -0
  33. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/__init__.py +1 -1
  34. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/context.py +9 -0
  35. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/harness.py +60 -11
  36. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/multiplexer.py +24 -1
  37. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/providers/base.py +4 -0
  38. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/providers/declarative.py +25 -5
  39. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/sandbox.py +6 -2
  40. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/bash.py +42 -2
  41. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/edit.py +88 -12
  42. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/web.py +71 -14
  43. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/unchanged.py +67 -0
  44. tether_sdk-0.3.0/tests/test_sandbox_mask_sockets.py +0 -18
  45. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/active/.gitkeep +0 -0
  46. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/active/north-star.md +0 -0
  47. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/active/run-progress.md +0 -0
  48. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/active/why-qwen-fails.md +0 -0
  49. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/archive/.gitkeep +0 -0
  50. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/archive/real-world-v0218.md +0 -0
  51. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/archive/v0-2-bugs.md +0 -0
  52. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/archive/work-on-v0-2-4.md +0 -0
  53. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/why-qwen-fails/reg_ab.sh +0 -0
  54. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/why-qwen-fails/regcount.py +0 -0
  55. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru/why-qwen-fails/whack.py +0 -0
  56. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.claude/gru.json +0 -0
  57. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.github/workflows/ci.yml +0 -0
  58. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.github/workflows/delivery.yml +0 -0
  59. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.github/workflows/docs.yml +0 -0
  60. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.gitignore +0 -0
  61. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.minion-err.txt +0 -0
  62. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/.minion-out.json +0 -0
  63. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/CONTRIBUTING.md +0 -0
  64. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/LICENSE +0 -0
  65. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/README.md +0 -0
  66. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/README.md +0 -0
  67. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/canary.py +0 -0
  68. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/canary_baseline.json +0 -0
  69. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/compare.py +0 -0
  70. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/grade.py +0 -0
  71. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/harness-bench/README.md +0 -0
  72. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/harness-bench/tether-adapter.patch +0 -0
  73. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/history.jsonl +0 -0
  74. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/base.py +0 -0
  75. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/baseline/harness.jsonl +0 -0
  76. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/baseline/ladder.jsonl +0 -0
  77. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/charts.py +0 -0
  78. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/general.py +0 -0
  79. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/probe.py +0 -0
  80. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/refs.py +0 -0
  81. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/report.py +0 -0
  82. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/run.py +0 -0
  83. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/snapshot.py +0 -0
  84. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/tasks.py +0 -0
  85. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/ladder/validate.py +0 -0
  86. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/little_coder_recipe.md +0 -0
  87. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/naive_agent.py +0 -0
  88. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/report.py +0 -0
  89. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/run.py +0 -0
  90. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/goal.txt +0 -0
  91. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/hidden/test_hidden.py +0 -0
  92. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ref/calc/__init__.py +0 -0
  93. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ref/calc/errors.py +0 -0
  94. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ref/calc/evaluator.py +0 -0
  95. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ref/calc/parser.py +0 -0
  96. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ref/calc/tokenizer.py +0 -0
  97. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/calc/__init__.py +0 -0
  98. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/calc/errors.py +0 -0
  99. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/calc/evaluator.py +0 -0
  100. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/calc/parser.py +0 -0
  101. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/calc/tokenizer.py +0 -0
  102. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/tests/__init__.py +0 -0
  103. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-hard/ws/tests/test_calc.py +0 -0
  104. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/goal.txt +0 -0
  105. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/hidden/test_hidden.py +0 -0
  106. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/cli.py +0 -0
  107. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/ledger/__init__.py +0 -0
  108. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/ledger/parser.py +0 -0
  109. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/ledger/report.py +0 -0
  110. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/sample.csv +0 -0
  111. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/tests/__init__.py +0 -0
  112. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-medium/ws/tests/test_report.py +0 -0
  113. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-simple/goal.txt +0 -0
  114. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-simple/hidden/test_hidden.py +0 -0
  115. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-simple/ws/tests/__init__.py +0 -0
  116. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-simple/ws/tests/test_textutils.py +0 -0
  117. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/code-simple/ws/textutils.py +0 -0
  118. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/codereview/goal.txt +0 -0
  119. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/codereview/ws/inventory.py +0 -0
  120. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/content/goal.txt +0 -0
  121. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/content/ws/.keep +0 -0
  122. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/goal.txt +0 -0
  123. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/hidden/test_hidden.py +0 -0
  124. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/AGENTS.md +0 -0
  125. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/README.md +0 -0
  126. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/docs/architecture.md +0 -0
  127. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/docs/style.md +0 -0
  128. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/mini/__init__.py +0 -0
  129. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/mini/cli.py +0 -0
  130. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/mini/fmt.py +0 -0
  131. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/mini/stats.py +0 -0
  132. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/mini/textio.py +0 -0
  133. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/tests/__init__.py +0 -0
  134. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/focus/ws/tests/test_stats.py +0 -0
  135. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/plan/goal.txt +0 -0
  136. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/plan/ws/README.md +0 -0
  137. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/plan/ws/svc/__init__.py +0 -0
  138. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/plan/ws/svc/client.py +0 -0
  139. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/plan/ws/svc/config.py +0 -0
  140. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/qa/goal.txt +0 -0
  141. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/qa/ws/.keep +0 -0
  142. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/review/goal.txt +0 -0
  143. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/bench/tasks/review/ws/paper.md +0 -0
  144. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/architecture.md +0 -0
  145. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/benchmarks.md +0 -0
  146. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/extending.md +0 -0
  147. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/glossary.md +0 -0
  148. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/guides/a-pipeline-with-checks.md +0 -0
  149. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/guides/add-your-own-tool.md +0 -0
  150. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/guides/fix-a-failing-test.md +0 -0
  151. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/guides/own-branch.md +0 -0
  152. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/guides/use-tether-from-your-agent.md +0 -0
  153. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/javascripts/mathjax.js +0 -0
  154. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/claude-haiku.md +0 -0
  155. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/general.md +0 -0
  156. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/img/harness-cost.svg +0 -0
  157. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/img/harness-pass.svg +0 -0
  158. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/img/task-grid.svg +0 -0
  159. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/img/tiers.svg +0 -0
  160. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/index.md +0 -0
  161. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/ladder/results.md +0 -0
  162. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/agent-interface.md +0 -0
  163. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/cli.md +0 -0
  164. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/context-and-store.md +0 -0
  165. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/harness.md +0 -0
  166. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/index.md +0 -0
  167. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/multiplexer-and-decorator.md +0 -0
  168. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/sandbox-and-skills.md +0 -0
  169. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/reference/tools.md +0 -0
  170. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/troubleshooting.md +0 -0
  171. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/agent-interface.md +0 -0
  172. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/cli-and-headless.md +0 -0
  173. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/index.md +0 -0
  174. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/multi-agent.md +0 -0
  175. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/sdk-and-custom-tools.md +0 -0
  176. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/security-and-sandbox.md +0 -0
  177. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/skills.md +0 -0
  178. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/docs/tutorial/workflows-and-telemetry.md +0 -0
  179. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/__init__.py +0 -0
  180. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/conftest.py +0 -0
  181. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/proxy.py +0 -0
  182. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_agents.py +0 -0
  183. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_command_retry.py +0 -0
  184. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_defaults.py +0 -0
  185. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_diff_other_writers.py +0 -0
  186. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_doctor.py +0 -0
  187. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_edit_write_blocks.py +0 -0
  188. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_empty_reply.py +0 -0
  189. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_escalation.py +0 -0
  190. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_export.py +0 -0
  191. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_faults.py +0 -0
  192. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_fold_window.py +0 -0
  193. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_gate_precheck.py +0 -0
  194. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_lifecycle.py +0 -0
  195. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_limits.py +0 -0
  196. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_mcp_workspace.py +0 -0
  197. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_network_pattern.py +0 -0
  198. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_notes.py +0 -0
  199. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_pipeline_resume.py +0 -0
  200. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_progress.py +0 -0
  201. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_quirks.py +0 -0
  202. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_replay.py +0 -0
  203. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_replay_worktree.py +0 -0
  204. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_restart.py +0 -0
  205. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox.py +0 -0
  206. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_allowlist.py +0 -0
  207. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_conformance.py +0 -0
  208. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_fail_closed.py +0 -0
  209. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_limits.py +0 -0
  210. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_mask.py +0 -0
  211. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_refusals.py +0 -0
  212. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_sandbox_stats.py +0 -0
  213. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_session_diff.py +0 -0
  214. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_surface.py +0 -0
  215. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_thinking.py +0 -0
  216. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_token_budget.py +0 -0
  217. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_wait_timeout.py +0 -0
  218. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_web_allowlist.py +0 -0
  219. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_worker_isolation.py +0 -0
  220. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_worker_reap_e2e.py +0 -0
  221. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_worktree.py +0 -0
  222. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/e2e/test_worktree_commit_excludes.py +0 -0
  223. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/README.md +0 -0
  224. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/checks.py +0 -0
  225. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/course.yaml +0 -0
  226. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/ruff.toml +0 -0
  227. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/skills/planner/SKILL.md +0 -0
  228. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/skills/searcher/SKILL.md +0 -0
  229. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/skills/searcher/tools.py +0 -0
  230. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/skills/writer/SKILL.md +0 -0
  231. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/examples/course/skills/writer/tools.py +0 -0
  232. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/pyproject.toml +0 -0
  233. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/scripts/record_golden.py +0 -0
  234. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/skills/researcher/SKILL.md +0 -0
  235. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/__init__.py +0 -0
  236. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/conftest.py +0 -0
  237. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/fixtures/dialects.jsonl +0 -0
  238. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/coding_edit_failure_modes.json +0 -0
  239. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/coding_edit_failure_modes.snap.json +0 -0
  240. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/done_nudge.json +0 -0
  241. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/done_nudge.snap.json +0 -0
  242. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/read_range_pingpong.json +0 -0
  243. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/read_range_pingpong.snap.json +0 -0
  244. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/research_web.json +0 -0
  245. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/research_web.snap.json +0 -0
  246. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/shell_wrappers.json +0 -0
  247. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/shell_wrappers.snap.json +0 -0
  248. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/waiting_input_resume.json +0 -0
  249. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/golden/waiting_input_resume.snap.json +0 -0
  250. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/__init__.py +0 -0
  251. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_anthropic.py +0 -0
  252. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_base.py +0 -0
  253. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_declarative.py +0 -0
  254. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_dialects.py +0 -0
  255. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_ollama.py +0 -0
  256. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_ollama_empty.py +0 -0
  257. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_openai.py +0 -0
  258. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/providers/test_retry.py +0 -0
  259. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_agent_interface.py +0 -0
  260. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_auto_test.py +0 -0
  261. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_bench_littlecoder.py +0 -0
  262. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_benchmarks_docs.py +0 -0
  263. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_cli.py +0 -0
  264. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_cli_columns.py +0 -0
  265. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_cli_docs.py +0 -0
  266. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_cli_wait_timeout.py +0 -0
  267. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_command_provider_retry.py +0 -0
  268. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_context.py +0 -0
  269. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_context_config.py +0 -0
  270. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_decorator.py +0 -0
  271. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_diff.py +0 -0
  272. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_diff_other_writers.py +0 -0
  273. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_docs_structure.py +0 -0
  274. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_doctor.py +0 -0
  275. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_doctor_extra.py +0 -0
  276. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_edit_only_shell.py +0 -0
  277. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_edit_tool.py +0 -0
  278. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_edit_write_search_replace.py +0 -0
  279. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_empty_reply_docs.py +0 -0
  280. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_empty_reply_review.py +0 -0
  281. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_escalation.py +0 -0
  282. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_escalation_fix.py +0 -0
  283. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_export.py +0 -0
  284. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_export_review.py +0 -0
  285. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_failing_test_source.py +0 -0
  286. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_fold_window.py +0 -0
  287. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_fold_window_cli.py +0 -0
  288. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_gate_precheck.py +0 -0
  289. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_gate_precheck_edges.py +0 -0
  290. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_gate_precheck_review.py +0 -0
  291. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_golden.py +0 -0
  292. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_guides_docs.py +0 -0
  293. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_integration.py +0 -0
  294. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_ladder_general.py +0 -0
  295. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_ladder_run.py +0 -0
  296. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_llms_txt.py +0 -0
  297. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_mcp_limits.py +0 -0
  298. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_mcp_workspace.py +0 -0
  299. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_misses.py +0 -0
  300. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_multiplexer.py +0 -0
  301. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_params.py +0 -0
  302. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_pipeline.py +0 -0
  303. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_pipeline_resume.py +0 -0
  304. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_progress.py +0 -0
  305. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_providers_docs.py +0 -0
  306. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_quiet_and_fold.py +0 -0
  307. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_read_thinking.py +0 -0
  308. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_readme.py +0 -0
  309. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_regression_note.py +0 -0
  310. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_release_hardening.py +0 -0
  311. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_repeat_reads.py +0 -0
  312. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_replay.py +0 -0
  313. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_replay_empty_model.py +0 -0
  314. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_restart.py +0 -0
  315. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_retro.py +0 -0
  316. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox.py +0 -0
  317. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_allowlist.py +0 -0
  318. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_allowlist_cov.py +0 -0
  319. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_conformance.py +0 -0
  320. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_fail_closed.py +0 -0
  321. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_limits.py +0 -0
  322. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_limits_cov.py +0 -0
  323. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_mask.py +0 -0
  324. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_refusals.py +0 -0
  325. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_stats.py +0 -0
  326. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_sandbox_stats_cov.py +0 -0
  327. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_security_docs.py +0 -0
  328. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_security_hardening.py +0 -0
  329. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_skills.py +0 -0
  330. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_small_rewrite.py +0 -0
  331. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_store.py +0 -0
  332. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_structured_history.py +0 -0
  333. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_test_reasoning.py +0 -0
  334. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_thinking.py +0 -0
  335. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_thinking_cap.py +0 -0
  336. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_token_budget.py +0 -0
  337. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_token_budget_crossing.py +0 -0
  338. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_trace.py +0 -0
  339. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_troubleshooting_docs.py +0 -0
  340. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_unchanged_write.py +0 -0
  341. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_web_allowlist.py +0 -0
  342. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_web_allowlist_cov.py +0 -0
  343. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worker_reap.py +0 -0
  344. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worktree.py +0 -0
  345. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worktree_commit_excludes.py +0 -0
  346. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worktree_docs.py +0 -0
  347. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worktree_errors.py +0 -0
  348. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/test_worktree_review.py +0 -0
  349. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/__init__.py +0 -0
  350. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_agent.py +0 -0
  351. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_agent_git_inherit.py +0 -0
  352. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_bash.py +0 -0
  353. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_done.py +0 -0
  354. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_edit.py +0 -0
  355. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tests/tools/test_mem.py +0 -0
  356. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/api.py +0 -0
  357. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/cli.py +0 -0
  358. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/decorator.py +0 -0
  359. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/diff.py +0 -0
  360. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/doctor.py +0 -0
  361. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/export.py +0 -0
  362. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/gates.py +0 -0
  363. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/mcp.py +0 -0
  364. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/pipeline.py +0 -0
  365. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/progress.py +0 -0
  366. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/providers/__init__.py +0 -0
  367. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/providers/anthropic.py +0 -0
  368. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/providers/ollama.py +0 -0
  369. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/providers/openai.py +0 -0
  370. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/retro.py +0 -0
  371. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/schema.py +0 -0
  372. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/skills.py +0 -0
  373. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/store.py +0 -0
  374. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/__init__.py +0 -0
  375. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/agent.py +0 -0
  376. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/done.py +0 -0
  377. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/tools/mem.py +0 -0
  378. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/trace.py +0 -0
  379. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/tether/worktree.py +0 -0
  380. {tether_sdk-0.3.0 → tether_sdk-0.3.1}/uv.lock +0 -0
@@ -55,7 +55,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
55
55
  - A task only completes when the agent explicitly calls `act("done", stdin="<final deliverable>")` (or `act("done '<short answer>'")`), pauses for human/parent input via `act("done --ask", stdin="<question>")` (transitioning the session to `WAITING_INPUT` and releasing the worker process until resumed via `send`), or hits `max_steps` (handed back as `WAITING_INPUT`, `send` resumes it with a fresh budget). A bare `done` (no answer, no stdin) completes with the result `Done.` and still runs the `verify` gates; `done --ask` with no question is an error (#169).
56
56
  - A bash command that needs approval to run outside the jail (#59) also pauses as `WAITING_INPUT` (`[APPROVAL NEEDED] <command>`, JSON `approval: {command, prefix}`); `send approve` runs it and resumes, any other reply refuses it.
57
57
  - If the model emits plain text without calling `act`, the harness appends a brief deterministic nudge reminding it to either execute a command or call `done`. A reply of 80 or more characters may be the answer: the nudge says it was not delivered and to pass it to `done`, and a session that still ends `WAITING_INPUT` (stuck or out of steps) carries the last such reply in its result, marked "never confirmed with `done`" (at most 2,000 chars, #173). Lookups by built-in tools (`web search|fetch`, `mem search|recall`, `agent status|wait|read|query`) count as reads, not changes.
58
- - After 6 consecutive read-only/verification steps following a change, the harness appends a `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete, repeated every 8 more quiet steps until the next change (0 static prompt tokens, #74, #132). Once per session, at 75% of `max_steps` (limits of 8 or more), a one-line `[NUDGE]` says how many steps are left (#133). Before any change, 12 read-only steps in a row (then every 8 more) append a one-line `[NUDGE]` to make the edit or call `done` (#127). Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change (#92) first get one `[WRAP UP]` turn (a read-only session is asked for its final answer, a session that already changed something is asked to call `done` with what is done and what is left, unless its `verify` gates or green test already finish it). Being stuck any way (the stall guard's warnings still unanswered after the wrap-up, or the same check command failing 3 times with an edit in between) restarts the session: one `restart` turn (event_type `restart`, at most 6000 chars: the files as the model left them, the last failing output, counts of what was tried; failed commands are not replayed) and the history continues from that turn alone. After 2 restarts, being stuck again hands the task back: the session ends `WAITING_INPUT` (exit code 2) with a `[STUCK ...]` summary as its result, so the caller can refine the request and `send` it. 0 static prompt tokens, no extra model call; `tether stop <id>` terminates a detached worker (#89). When that restart comes from the same check command failing 3 times, the files the model changed go back to how they started (a fresh attempt, Olausson et al. ICLR 2024) and the message names the failing tests instead of quoting the failed code; files it created stay. A run counts as near a pass when at most 3 tests fail (counted from the runner's summary line, so `| head` cannot hide failures) and more than twice that many pass: the harness keeps that version, and once 3 later runs fail to beat it, its files go back in place with one `[RESTORED]` line and the history stays (no restart turn, #173).
58
+ - After 6 consecutive read-only/verification steps following a change, the harness appends a `[NUDGE]` to that step's observation reminding the model to call `done` if the task is complete, repeated every 8 more quiet steps until the next change (0 static prompt tokens, #74, #132). Once per session, at 75% of `max_steps` (limits of 8 or more), a one-line `[NUDGE]` says how many steps are left (#133); without a change it asks for the answer instead of an edit. The step before the limit gets a one-line `[LAST STEP]` notice, once (limits of 8 or more). Before any change, 12 read-only steps in a row (then every 8 more) append a one-line `[NUDGE]` to make the edit or call `done` (#127); with neither `bash` nor `edit` enabled it asks for the answer or one more lookup. Seven reads of one file in a row with no change are a `cycle_stall`, and 4 stall warnings without a change (#92) first get one `[WRAP UP]` turn (a read-only session is asked for its final answer, a session that already changed something is asked to call `done` with what is done and what is left, unless its `verify` gates or green test already finish it). Being stuck any way (the stall guard's warnings still unanswered after the wrap-up, or the same check command failing 3 times with an edit in between) restarts the session: one `restart` turn (event_type `restart`, at most 6000 chars: the files as the model left them, the last failing output, counts of what was tried; failed commands are not replayed) and the history continues from that turn alone. After 2 restarts, being stuck again hands the task back: the session ends `WAITING_INPUT` (exit code 2) with a `[STUCK ...]` summary as its result, so the caller can refine the request and `send` it. 0 static prompt tokens, no extra model call; `tether stop <id>` terminates a detached worker (#89). When that restart comes from the same check command failing 3 times, the files the model changed go back to how they started (a fresh attempt, Olausson et al. ICLR 2024) and the message names the failing tests instead of quoting the failed code; files it created stay. A run counts as near a pass when at most 3 tests fail (counted from the runner's summary line, so `| head` cannot hide failures) and more than twice that many pass: the harness keeps that version, and once 3 later runs fail to beat it, its files go back in place with one `[RESTORED]` line and the history stays (no restart turn, #173).
59
59
  - When `verify` gate commands are configured (via `Harness(verify=[...])`, CLI `--verify`, or Skill YAML `verify: [...]`), `Harness.execute_session` deterministically executes each command inside the session's `SandboxPolicy` before allowing `done` to transition to `COMPLETED` (consuming 0 static prompt tokens until a gate fails). A gate that errors instead of failing (sandbox violation, exit 124/126/127) ends the session `FAILED` at once as `gate_error` ("gate misconfigured"), without counting toward the consecutive-failure abort. Before the first model call every gate is pre-checked (`tether/gates.py`): an empty gate, one that cannot be parsed, or a program that does not exist (every command of a `&&`/`||`/`;`/`|` chain; relative paths are looked up in the workspace) ends the session the same way with no tokens spent (#82). When the stall guard is about to restart or hand back a session and gates are configured, it runs them first: a change was made and every gate passes, so the session ends `COMPLETED` ("Finished by the harness"), since the deterministic gate is the completion criterion the caller asked for (#163). With no gates, the model's own last passing test run (runner-recognised, nothing changed since, still green when re-run) is the implicit gate, at a stall and at the step cap; a passing run after a change also appends a one-line `[NUDGE]` to call `done` (#177).
60
60
 
61
61
  ### Rule 4: Progressive `--help` (JIT Schema Disclosure)
@@ -75,6 +75,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
75
75
  - Exception: the latest read of each of the last 8 files (`edit view <p>`, `bash cat/head/tail/sed -n <p>`; paths normalized against the workspace) is not folded until `<p>` is edited (`edit write|append|replace`, or any other bash command naming it: `sed -i`, `tee`, redirects, `python -c`) or re-read. The head+tail elision still applies (an observation may use up to a third of `max_context_tokens`, at least 2,400 chars), and under `max_context_tokens` pressure it is folded like any other (oldest first). The `mem recall` hint is shown only when `mem` is enabled (else "re-run the command").
76
76
  - A recognised test command piped to `head` (`<tests> 2>&1 | head -N`, nothing else piped) runs without the pipe and is cut by `bash` itself: first N lines, a `[... K lines cut by head ...]` marker, the last 6 lines and the tests' own `[exit N]`, so the summary and exit code survive (0 static prompt tokens, #173).
77
77
  - When a recognised test command fails, the harness appends the source of up to 3 failing test functions named in the traceback (`[failing test source]`, workspace files only, at most 1,500 chars, skipped when the output already shows it) so the model sees the test's inputs (0 static prompt tokens, no model call, #173). When the same suite ran before (same test count) and tests that passed then fail now, one `[regression]` line names them and the newly passing ones (at most 5 names each, 400 chars, #173). When a command that writes existing workspace files (`edit write|replace <p>`, `> p`, `>> p`, `tee p`) leaves all of them with the same code (identical bytes, or for `.py` an identical AST), one `[NUDGE]` line says so (`tether/unchanged.py`, at most 200 chars, 0 static prompt tokens, #173). A whole-file rewrite (`edit write p`, `> p`) of a file of 20 or more lines that changed at most 20% of them gets one `[NUDGE]` pointing to `edit replace p`, once per file per session (#173).
78
+ - A `bash` command that leaves a workspace `.py` file unparsable (it parsed before the command) is undone: the old bytes are written back and one `[REVERTED]` line per file (at most 3) says so, before the auto-test and the notes run (F5, `tether/unchanged.py` `broken_by_command`, 0 static prompt tokens, no model call). `edit` commands are not reverted (`edit` warns on its own).
78
79
  - Repair loop around tests (#173, 0 static prompt tokens, no model call): once the model has run a recognised test command, the harness re-runs it after each command that writes an existing workspace file and appends the result (`[auto-test]`, Aider's auto-test), so no version is typed blind. When the model first reads the test file of a starting file (`x_test.py`/`test_x.py` next to `x.py`), one `[BEFORE WRITING]` line asks it to state in plain words what the 3 hardest tests call and expect (AlphaCodium, Ridnik et al. 2024); a plain-text reply then is the expected answer, not a nudge. If it writes first, that first rewrite is held once with the same question. When the same tests fail again after a change, or at most 3 fail, a runtime trace of the smallest failing test is appended once per failing set (`tether/trace.py`: calls into workspace code with arguments and results, 2 levels deep, at most 30 lines / 1,500 chars; LDB, Zhong et al. ACL 2024). The first near pass also gets one `[NUDGE]` pointing to `edit replace`.
79
80
  - History is replayed as structured `tool_calls` + `tool` results (providers with `structured_history = True`; textual `act('...')` form only as fallback), with `stdin` replayed as a real 400-char prefix, never a `<N chars>` placeholder. A call and its result are trimmed together.
80
81
 
@@ -130,7 +131,7 @@ tether/
130
131
  ├── done.py # Mandatory explicit completion command (`done`)
131
132
  ├── bash.py # Kernel-sandboxed, env-scrubbed, workspace-bounded shell execution
132
133
  ├── web.py # Zero-config DuckDuckGo Lite httpx search (+ Brave/Tavily/SearXNG env override) & reader
133
- ├── edit.py # Workspace-jailed file viewing (`view`) & stdin patching (`write`, `append`, `replace`)
134
+ ├── edit.py # Workspace-jailed file viewing (`view`) & stdin patching (`write`, `append`, `insert`, `replace`)
134
135
  ├── mem.py # Working scratchpad (`pin`/`unpin`), FTS5 search (`search`), & recall (`recall`)
135
136
  └── agent.py # Async multi-agent orchestration (`spawn`, `status`, `wait`, `query`, `read`, `send`)
136
137
  ```
@@ -234,7 +235,7 @@ uv run mypy tether/
234
235
  2. **Async-First Design:**
235
236
  - All model calls and I/O-bound tool executions must be `async`/`await` native, while seamlessly wrapping synchronous user `@tool` functions.
236
237
  3. **Graceful Degradation on 9B Hallucinations:**
237
- - Small models occasionally emit `cmd` with minor quirks (e.g., leading `$`, wrapping backticks, or calling a tool name directly instead of `act`). The multiplexer must normalize trivial syntax quirks deterministically before failing. This includes unescaping literal `\n`/`\t` in one-line `stdin` for `edit write`/`edit replace` and `done` (never `bash`), turning a trailing shell heredoc in `cmd` (`edit write f <<'EOF' ... EOF`) into `stdin` (never for `bash`), rewriting `edit --file p --content c` flags into `edit write p` + stdin (and `edit write p <text>` with no stdin into `edit write p` + `<text>` as typed) and `done --stdin 'x'` into `done` + stdin, unwrapping `bash -c/-lc '<cmd>'` and `sh -c` wrappers, unwrapping an `act("<cmd>", stdin="...")` call typed as text in `cmd` (also after `bash`) into the command and its `stdin` (#169), running an unknown first word through the enabled `bash` tool (never for disabled built-ins), running a bash `cd <missing dir> && <cmd>` as `<cmd>` in the workspace with a one-line note (#79), running plain shell file writes/reads as `edit` when `edit` is on and `bash` is off (`echo t > p`, `printf t > p`, `cat > p` + heredoc -> `edit write p`, with `>>` -> `edit append p`; `cat p` -> `edit view p`), registering extra quirk fixers with `register_normalizer`, mapping a native tool call named other than `act` (also written as plain-text JSON `{"name": ..., "arguments": {...}}`) to `<name> --k 'v'` so the JIT usage answers it, inferring the `edit` verb (`edit <path>` -> view/replace/write) and accepting `apply_patch` (`*** Begin Patch` or unified-diff patches, also when a diff arrives as `edit <path>` stdin) as an `edit` verb.
238
+ - Small models occasionally emit `cmd` with minor quirks (e.g., leading `$`, wrapping backticks, or calling a tool name directly instead of `act`). The multiplexer must normalize trivial syntax quirks deterministically before failing. This includes unescaping literal `\n`/`\t` in one-line `stdin` for `edit write`/`edit replace` and `done` (never `bash`), turning a trailing shell heredoc in `cmd` (`edit write f <<'EOF' ... EOF`) into `stdin` (never for `bash`), rewriting `edit --file p --content c` flags into `edit write p` + stdin (and `edit write p <text>` with no stdin into `edit write p` + `<text>` as typed) and `done --stdin 'x'` into `done` + stdin, unwrapping `bash -c/-lc '<cmd>'` and `sh -c` wrappers, unwrapping an `act("<cmd>", stdin="...")` call typed as text in `cmd` (also after `bash`) into the command and its `stdin` (#169), running an unknown first word through the enabled `bash` tool (never for disabled built-ins), running a bash `cd <missing dir> && <cmd>` as `<cmd>` in the workspace with a one-line note (#79), moving a path typed inside the quotes of `sed -n 'a,bp file'` out of them (#270), running plain shell file writes/reads as `edit` when `edit` is on and `bash` is off (`echo t > p`, `printf t > p`, `cat > p` + heredoc -> `edit write p`, with `>>` -> `edit append p`; `cat p` -> `edit view p`), registering extra quirk fixers with `register_normalizer`, mapping a native tool call named other than `act` (also written as plain-text JSON `{"name": ..., "arguments": {...}}`) to `<name> --k 'v'` so the JIT usage answers it, inferring the `edit` verb (`edit <path>` -> view/replace/write) and accepting `apply_patch` (`*** Begin Patch` or unified-diff patches, also when a diff arrives as `edit <path>` stdin) as an `edit` verb.
238
239
  4. **Golden traces:** behaviour the model sees (framing, folding, nudges, tool output) is locked by `tests/golden/*.snap.json`. A PR that changes it runs `UPDATE_GOLDEN=1 pytest tests/test_golden.py` and the snapshot diff is part of the review; unrelated traces must not change. See `CONTRIBUTING.md`.
239
240
  5. **E2E scenarios:** every feature or bug fix ships with a scenario in `e2e/` (real CLI, real ollama behind a fault-injecting proxy, local only, `TETHER_E2E=1 uv run pytest e2e`). Assert on what tether sends and records, never on the model's skill; script the reply or inject the fault so the test does not depend on model potency. **Before merging any PR, run `TETHER_E2E=1 uv run pytest e2e` locally and put the result in the PR description (CI cannot run it). Every failure must be mitigated before merge: fix the code, or fix a wrong scenario, or record in the PR why it is acceptable and what follows up. A PR is not merged with an unexplained red scenario.** See `CONTRIBUTING.md`.
240
241
  6. **Documentation Integrity:**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tether-sdk
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: General-purpose, headless-first LLM-Native micro-harness for small language models
5
5
  Project-URL: Homepage, https://github.com/igormcsouza/tether
6
6
  Project-URL: Repository, https://github.com/igormcsouza/tether
@@ -0,0 +1,88 @@
1
+ # Write tasks a small model can finish
2
+
3
+ *For: people who hand tasks to tether on a small local model, and people choosing a model for a job. After this page you can write a task that a 9B model finishes, check that it really did, and tell whose fault a failed task is.*
4
+
5
+ A small model is a good typist and a careful rule follower. It is a weak planner. So the plan goes in the task text, and the model does one small thing at a time.
6
+
7
+ ## The rules
8
+
9
+ 1. **One edit per task.** One file, one place. Two changes are two tasks.
10
+ 2. **Say where.** Name the file and the line numbers, and tell the model to view those lines first.
11
+ 3. **Give a contract, not code.** Say what the function must return and show two or three examples. The model writes the body. If you already wrote the code, apply it yourself.
12
+ 4. **Avoid line arithmetic.** New code goes in with `edit append <file>`, which needs no line numbers. To add lines in the middle, replace one short line with itself first and the new lines after it.
13
+ 5. **Pass the edit as two arguments.** `cmd` is `edit replace <file> --lines A:B` and `stdin` is the final text of those lines. No diff marks, no code fences.
14
+ 6. **One check that runs code.** Give one command that fails before the change and passes after it. Compare exact values. A `grep` for a word passes on broken code.
15
+ 7. **Keep chores out.** Lint, types and the full test suite run after the task, by you or by `--verify`.
16
+ 8. **Small step budget.** `--max-steps 8` to `12`. A task that needs more is two tasks.
17
+
18
+ ## Example
19
+
20
+ A function forgets to add 32. This is `temps.py`:
21
+
22
+ ```python
23
+ def to_fahrenheit(celsius):
24
+ return celsius * 9 / 5
25
+ ```
26
+
27
+ `check.py` fails now and passes after the fix:
28
+
29
+ ```python
30
+ from temps import to_fahrenheit
31
+
32
+ assert to_fahrenheit(0) == 32
33
+ assert to_fahrenheit(100) == 212
34
+ assert to_fahrenheit(-40) == -40
35
+ print("ok")
36
+ ```
37
+
38
+ The task, in `task.md`:
39
+
40
+ ```text
41
+ You do ONE small edit. Follow the steps in order. Do nothing else.
42
+ 1. View: `edit view temps.py --lines 1:2`. Line 2 returns the wrong value: it forgets to add 32.
43
+ 2. Fix line 2 so `to_fahrenheit(0)` is 32 and `to_fahrenheit(100)` is 212. Call the `act` tool with two arguments: cmd = `edit replace temps.py --lines 2:2`, stdin = the corrected line (4 spaces of indentation).
44
+ 3. Run `bash python3 check.py`. It prints `ok` -> call `act("done", stdin="fixed")`. It fails -> view line 2, fix it the same way, run the check again.
45
+ ```
46
+
47
+ Run it:
48
+
49
+ ```bash
50
+ tether run --model ollama/qwen3.5:9b --tools bash,edit --max-steps 8 --verify "python3 check.py" "$(cat task.md)"
51
+ ```
52
+
53
+ ```text
54
+ session ses_3353ac87
55
+ fixed
56
+ ```
57
+
58
+ The session took 4 of its 8 steps. `--verify` ran `check.py` again before `done` was accepted, so `fixed` means the check passed, not that the model said so.
59
+
60
+ ## What we saw, by model
61
+
62
+ | Model | Does well | Does badly |
63
+ |---|---|---|
64
+ | `ollama/qwen3.5:9b` | Follows numbered steps exactly. Writes a short function body from a contract. Writes a test from a described case. Fixes its own code when the check fails. | Counting lines after an edit. Long tasks with several goals. Side rules such as a line-length limit. It follows a wrong task as faithfully as a right one. |
65
+ | `ollama/qwen3.5:4b` | Single one-line edits. | Leaves the steps and does something else often enough that we do not rely on it. |
66
+ | Claude Haiku | Plans, tests written before the code, QA and review of a finished change. | Writing tasks for a smaller model: wrong line counts, checks that could not fail. Its tests need a second look. |
67
+ | Claude Sonnet, Claude Opus | No data yet for this way of working. | No data yet. |
68
+
69
+ This table changes as tether and the models change. A row is added only from runs we looked at.
70
+
71
+ ## When it goes wrong
72
+
73
+ Read the session with `tether export <id>` and ask three questions, in this order:
74
+
75
+ 1. **Did a tool refuse or mangle a correct command?** That is tether. Look for an error after a command that looks right, and report it.
76
+ 2. **Did the model do what the task said, and the result is still wrong?** That is the task. Fix the text: a wrong line number, a missing step, a check that cannot fail.
77
+ 3. **Did the model leave a clear task?** That is the model. Make the task smaller once. If it still leaves it, use a larger model for that task.
78
+
79
+ Common signs:
80
+
81
+ | Sign | Likely cause | What to do |
82
+ |---|---|---|
83
+ | Out of steps, many views and replaces of the same lines | Line arithmetic | Use `edit append`, or replace one short line |
84
+ | `done`, but the file is broken | The check did not run the code | Make the check import and call it |
85
+ | The check passes, the output is slightly off | The check compares part of the output | Compare the whole value |
86
+ | An edit command is refused | The edit text was put inside `cmd` | Say "two arguments: cmd and stdin" in the task |
87
+
88
+ More: [Fix a failing test with a local model](fix-a-failing-test.md), [Troubleshooting](../troubleshooting.md).
@@ -18,6 +18,7 @@ Most agent frameworks drown small models in 3,000–8,000 tokens of nested JSON
18
18
  | I want to... | Read |
19
19
  |---|---|
20
20
  | Have a small local model fix a failing test | [Fix a failing test](guides/fix-a-failing-test.md) |
21
+ | Write a task a small model can finish | [Write tasks a small model can finish](guides/small-models.md) |
21
22
  | Delegate work from Claude Code, Codex or Cursor | [Use tether from your agent](guides/use-tether-from-your-agent.md) |
22
23
  | Check my setup before the first run | `tether doctor` in the [CLI reference](reference/cli.md) |
23
24
  | Let a session work on its own branch | [Run a session on its own branch](guides/own-branch.md) |
@@ -5,6 +5,7 @@
5
5
  ## Start here
6
6
  - [Home](https://igormcsouza.github.io/tether/): what tether is, install, first run.
7
7
  - [Fix a failing test with a local model](https://igormcsouza.github.io/tether/guides/fix-a-failing-test/): a verification gate decides when the job is done.
8
+ - [Write tasks a small model can finish](https://igormcsouza.github.io/tether/guides/small-models/): task-writing rules, a re-runnable example, what each model does well and badly.
8
9
  - [Use tether from Claude Code, Codex or Cursor](https://igormcsouza.github.io/tether/guides/use-tether-from-your-agent/): MCP setup, the JSON result, exit codes.
9
10
  - [A pipeline where each stage has its own job](https://igormcsouza.github.io/tether/guides/a-pipeline-with-checks/): stages, per-stage model, tools and gate.
10
11
  - [Give the model a tool of your own](https://igormcsouza.github.io/tether/guides/add-your-own-tool/): turn a Python function into a command.
@@ -123,6 +123,7 @@ A `command` entry wraps any program that takes a prompt and prints JSON.
123
123
 
124
124
  - **Placeholders**: `{model}`, `{system}`, `{conversation}` and `{schema}` (the act JSON schema, compact) are substituted in `argv` items and in the `stdin` template. If `stdin` is omitted it defaults to `{conversation}`.
125
125
  - **No shell**: `argv` is executed directly, never through a shell.
126
+ - **Failures**: a non-zero exit with output fails the session at once with ``Worker error: ProviderError: command provider `<model>` failed (exit N): <text>``. The text is the provider's own words: the `reply` field when the output is JSON, else its stdout or stderr, at most 300 characters. It is not retried, so a usage or rate limit costs one call: wait for the reset or pass another `--model`.
126
127
  - **Retries**: a timeout, or a non-zero exit with no output, is retried twice with 1 s then 2 s backoff; after the third failure Tether raises `ProviderError` naming the provider, model and timeout. A non-zero exit with output, or an unparsable reply, is not retried.
127
128
  - **Reply**: `reply` is a dotted path into the JSON the command prints (`result` in the example).
128
129
  - **Usage**: `usage.prompt` is a path or a list of paths (their values are summed); `usage.completion` is a path. Both are optional.
@@ -1,5 +1,28 @@
1
1
  # Release Notes
2
2
 
3
+ ## v0.3.1 (2026-11-14)
4
+
5
+ ### Added
6
+
7
+ - `edit insert <path> --line N` adds the stdin lines before line N and replaces nothing, so a small model no longer loses line N when it adds lines (#320). A bare `edit replace <path> --lines a:b` with empty stdin now says it deleted the lines (#321), and `edit append` and `insert` always end the file with a newline.
8
+
9
+ ### Fixed
10
+
11
+ - A session whose only edits were refused (wrong command shape, sandbox refusal) is no longer ended as `COMPLETED` "Finished by the harness ... after its change": a refused command ran nothing, so it does not count as a change and the session is handed back as `WAITING_INPUT` (#318).
12
+ - `edit replace <path>` now applies a unified diff or bare `@@` hunks sent as stdin through the same applier as `edit <path>`; a `, stdin="..."` tail typed inside the command is moved into stdin; and `edit replace <path> --lines a:b` applies a marked `-`/`+` diff of exactly those lines, so a correct edit is no longer refused or mis-applied (#317).
13
+ - A headless worker no longer imports workspace code through a relative or empty `PYTHONPATH` entry: only absolute entries are passed on (#283).
14
+ - The sandbox no longer fails every `bash` call with `bwrap: Can't create file at /run/...` when `/run` holds a symlink to a unix socket (e.g. `/run/php/php-fpm.sock`): the socket the link points to is masked, once.
15
+ - `sed -n '45,90p file'` (the path typed inside the quotes) now runs as `sed -n '45,90p' file` instead of failing (#270).
16
+ - A `command` provider that hits a usage or rate limit now fails at once with the provider's own words and a next step (`wait for the reset or pass another --model`), instead of a raw JSON error after retries (#305).
17
+ - A `bash` command that times out after starting a background job (`server &`) now says why and what to do: run it in the foreground with `timeout N <cmd>`, or send its output away with `<cmd> >/dev/null 2>&1 &` (#319).
18
+ - `web search` now says when the search engine refuses (HTTP 202 or a bot-check page) instead of printing `No search results found.`, so a model stops retrying. `web fetch <url>#anchor` starts the returned text at that element, and `web fetch <url> --from <text>` starts it at the first match. A cut page names the `--from` words to continue with.
19
+ - The harness tells a session that only looks things up to answer, not to edit: the step before the limit gets a `[LAST STEP]` notice, the 75% and 12-step nudges ask for an answer when neither `bash` nor `edit` is enabled, and a restart or failed `verify` gate after a check failed 3 times names `done --ask` as the way out of a wrong test.
20
+ - A `bash` command that leaves a Python file unparsable is undone: the file goes back to its bytes from before the command, and the observation names it in a `[REVERTED]` line so the model redoes the change in smaller steps (F5).
21
+
22
+ ### Docs
23
+
24
+ - New guide [Write tasks a small model can finish](guides/small-models.md): the task-writing rules, a re-runnable example, what each model was seen to do well and badly, and how to tell whose fault a failed task is.
25
+
3
26
  ## v0.3.0 (2026-11-09)
4
27
 
5
28
  ### Breaking changes
@@ -37,6 +37,8 @@ act(cmd="bash", stdin="python3 - << 'EOF'\nimport math\nprint(math.pi)\nEOF")
37
37
 
38
38
  A test run piped to `head` (`python3 -m unittest 2>&1 | head -50`) still shows how it ended: tether keeps the first lines, marks what it cut, and adds the last lines with the summary and the tests' own exit code.
39
39
 
40
+ A command that times out after starting a background job (`server &`) gets one more line saying so, with the two fixes: run it in the foreground with `timeout N <cmd>`, or send its output away with `<cmd> >/dev/null 2>&1 &`.
41
+
40
42
  ## `edit` — Workspace-Jailed File Operations
41
43
 
42
44
  Provides deterministic line-numbered viewing and `stdin`-based writing/replacement without sed/awk quoting bugs:
@@ -62,8 +64,12 @@ Zero-config web search via DuckDuckGo Lite (or automatic override via `SEARXNG_U
62
64
  ```text
63
65
  act(cmd='web search "SQLite WAL concurrency" --max 5')
64
66
  act(cmd="web fetch https://www.sqlite.org/wal.html")
67
+ act(cmd="web fetch https://docs.python.org/3/library/stdtypes.html#str.splitlines")
68
+ act(cmd="web fetch https://www.sqlite.org/wal.html --from 'checkpoint'")
65
69
  ```
66
70
 
71
+ `#anchor` starts the text at that element, `--from <text>` at the first match; a cut page prints the `--from` words to continue with.
72
+
67
73
  ## `mem` — Pinned Scratchpad & BM25 Observation Recall
68
74
 
69
75
  When raw tool outputs age past $K=2$ turns and the history fills more than half of the context budget, Tether folds them into 1-line tombstones (`[obs_3 folded: ...]`). The `mem` tool lets the agent pin key facts into active context or search/recall folded observations from SQLite FTS5:
@@ -89,7 +95,7 @@ act(cmd="mem recall obs_3")
89
95
 
90
96
  The [course-builder example](https://github.com/igormcsouza/tether/tree/main/examples/course) uses `edit` and custom tools to turn a plan into a course. Three limits shaped it:
91
97
 
92
- - **`edit write` replaces the whole file.** Overwriting an existing file now reports `Replaced N lines with M (X removed, Y added; first removed: ...)` and, when the file shrank, points to `edit replace --lines a:b` ([#40](https://github.com/igormcsouza/tether/issues/40)). Small models assume it appends and silently lose earlier content. To build a file in parts, use `edit append <path>` with the next part in `stdin`; it creates the file if needed and adds a newline when the file does not end with one.
98
+ - **`edit write` replaces the whole file.** Overwriting an existing file now reports `Replaced N lines with M (X removed, Y added; first removed: ...)` and, when the file shrank, points to `edit replace --lines a:b` ([#40](https://github.com/igormcsouza/tether/issues/40)). Small models assume it appends and silently lose earlier content. To build a file in parts, use `edit append <path>` with the next part in `stdin`; it creates the file if needed and adds a newline when the file does not end with one. To add lines in the middle, use `edit insert <path> --line N` with the lines in `stdin`: they go before line N and nothing is replaced (N = lines + 1 appends).
93
99
  - **`web search` cannot reliably find a specific YouTube video.** Use a [custom tool](sdk-and-custom-tools.md#pattern-numbered-candidates) that returns numbered candidates instead.
94
100
  - **Tools are opt-in.** The searcher skill lists `tools: [yt-todo, yt-candidates, yt-save]` and nothing else; `edit` is only loaded by the stages that write files.
95
101
 
@@ -0,0 +1,15 @@
1
+ """#319: a timed-out background job says why. Scripted: the replies never depend on the model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from e2e.conftest import Tether
6
+
7
+ HINT = "A background job (`&`) keeps the command open."
8
+
9
+
10
+ def test_timed_out_background_job_says_why(tether: Tether, chaos) -> None:
11
+ # one 30 s wait: the sandbox kills the job at the timeout; the result must carry the hint
12
+ chaos.script(("bash sleep 60 & wait", None), ("done", "finished"))
13
+ r = tether.run("background", "--tools", "bash", "--max-steps", "4")
14
+ outputs = tether.tool_outputs(r["id"])
15
+ assert any("command timed out" in o and HINT in o for o in outputs), outputs
@@ -0,0 +1,30 @@
1
+ """F5: a bash command that breaks a Python file is undone and the model is told.
2
+
3
+ Scripted: the replies are fixed, so the result never depends on what the model answers.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from e2e.conftest import Tether
9
+
10
+ GOOD = "def f(x):\n return x + 1\n"
11
+
12
+
13
+ def test_a_bash_write_that_breaks_python_is_undone(tether: Tether, chaos) -> None:
14
+ # the first command leaves m.py unparsable (a printf escape gone wrong); the harness must put
15
+ # the old bytes back before the model's next step
16
+ chaos.script(
17
+ ("printf 'def f(x):\\n return x +\\n' > m.py", None),
18
+ ("ls", None),
19
+ )
20
+ (tether.ws / "m.py").write_text(GOOD)
21
+ tether.run("broken", "--tools", "bash", "--max-steps", "2")
22
+ assert (tether.ws / "m.py").read_text() == GOOD
23
+
24
+
25
+ def test_a_sed_i_that_breaks_python_is_undone(tether: Tether, chaos) -> None:
26
+ # sed -i is not a redirect: the file is found because the command names it
27
+ chaos.script(("sed -i 's/return x/return (x/' m.py", None), ("ls", None))
28
+ (tether.ws / "m.py").write_text(GOOD)
29
+ tether.run("broken", "--tools", "bash", "--max-steps", "2")
30
+ assert (tether.ws / "m.py").read_text() == GOOD
@@ -0,0 +1,14 @@
1
+ """#320: `edit insert` adds lines without replacing the kept line. Scripted: the proxy forces the
2
+ edit calls, so the result never depends on what the model answers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from e2e.conftest import Tether
7
+
8
+
9
+ def test_scripted_insert_keeps_every_line(tether: Tether, chaos) -> None:
10
+ # one insert before line 2, then the run ends at the step limit
11
+ chaos.script(("edit insert f.txt --line 2", "X\n"), *[("edit view f.txt", None)] * 12)
12
+ (tether.ws / "f.txt").write_text("a\nb\nc\n")
13
+ tether.run("insert", "--tools", "edit", "--verify", "true", "--max-steps", "10")
14
+ assert (tether.ws / "f.txt").read_text() == "a\nX\nb\nc\n"
@@ -0,0 +1,15 @@
1
+ """#317: a correct diff sent to `edit replace <path>` is applied. Scripted: the proxy answers
2
+ with a fixed command, so the result never depends on what the model says."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from e2e.conftest import Tether
7
+
8
+ DIFF = "@@ -1,3 +1,3 @@\n x = 1\n-y = 2\n+y = 20\n z = 3\n"
9
+
10
+
11
+ def test_diff_sent_to_replace_is_applied(tether: Tether, chaos) -> None:
12
+ chaos.script(("edit replace f.py", DIFF))
13
+ (tether.ws / "f.py").write_text("x = 1\ny = 2\nz = 3\n")
14
+ tether.run("shapes", "--tools", "edit", "--verify", "true", "--max-steps", "3")
15
+ assert (tether.ws / "f.py").read_text() == "x = 1\ny = 20\nz = 3\n"
@@ -0,0 +1,19 @@
1
+ """F3 last-step notice: a scripted lookup-only session is told on its last-but-one step to answer.
2
+
3
+ Scripted replies, so the result never depends on the model.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from e2e.conftest import Tether
9
+
10
+
11
+ def test_last_step_notice_reaches_a_lookup_only_session(tether: Tether, chaos) -> None:
12
+ # 9 lookups within 10 steps: the notice lands on step 9, the one before the limit
13
+ chaos.script(*[f"ls {i}" for i in range(9)])
14
+ r = tether.run("look around", "--tools", "bash", "--max-steps", "10")
15
+ outs = tether.tool_outputs(r["id"])
16
+ notices = [o for o in outs if "[LAST STEP]" in o]
17
+ assert len(notices) == 1, outs[-3:]
18
+ assert "Call `done` now with your answer" in notices[0]
19
+ assert r["rc"] in (0, 2), r
@@ -0,0 +1,39 @@
1
+ # Provider usage limit (#305), end to end: a command provider reports its usage limit.
2
+ # Scripted: no model is used; the command is a local script.
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ from e2e.conftest import Tether
12
+
13
+ LIMIT = """import json, os, sys
14
+ counter = sys.argv[1]
15
+ n = int(open(counter).read()) if os.path.exists(counter) else 0
16
+ open(counter, "w").write(str(n + 1))
17
+ sys.stdin.read()
18
+ print(json.dumps({"is_error": True, "result": "Claude usage limit reached. Resets 1:30pm"}))
19
+ sys.exit(1)
20
+ """
21
+
22
+
23
+ def test_usage_limit_fails_at_once_with_provider_words(tether: Tether, tmp_path: Path) -> None:
24
+ counter = tmp_path / "counter"
25
+ script = tmp_path / "limit.py"
26
+ script.write_text(LIMIT)
27
+ providers = tmp_path / "providers.yaml"
28
+ entry = {
29
+ "kind": "command",
30
+ "argv": [sys.executable, str(script), str(counter)],
31
+ "stdin": "{conversation}",
32
+ "reply": "result",
33
+ }
34
+ providers.write_text(yaml.safe_dump({"e2elimit": entry}))
35
+ r = tether.run("answer", prefix="e2elimit")
36
+ assert r["status"] == "FAILED", r
37
+ assert "usage limit reached" in r["failure"]["message"], r
38
+ assert "--model" in r["failure"]["message"], r
39
+ assert counter.read_text() == "1", "a usage limit is not retried"
@@ -0,0 +1,16 @@
1
+ """#318: a refused command is not a change. Scripted: the proxy forces refused edits (no stdin),
2
+ so the result never depends on the model; the session must end WAITING_INPUT, not COMPLETED."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from e2e.conftest import Tether
7
+
8
+
9
+ def test_refused_edits_do_not_finish_as_completed(tether: Tether, chaos) -> None:
10
+ # 12 refused edits outlast the 10 steps: the stall guard must not finish on the green gate
11
+ chaos.script(*[("edit replace f.py --lines 1:1", None)] * 12)
12
+ (tether.ws / "f.py").write_text("x = 1\n")
13
+ r = tether.run("refused", "--tools", "edit", "--verify", "true", "--max-steps", "10")
14
+ assert (tether.ws / "f.py").read_text() == "x = 1\n"
15
+ assert (r["rc"], r["status"]) == (2, "WAITING_INPUT"), r
16
+ assert "Finished by the harness" not in (r["result"] or ""), r
@@ -0,0 +1,14 @@
1
+ """sed -n 'a,bp file' with the path typed inside the quotes runs as sed -n 'a,bp' file (#270)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from e2e.conftest import Tether
6
+
7
+
8
+ def test_sed_with_path_inside_quotes_runs(tether: Tether, chaos) -> None:
9
+ (tether.ws / "f.txt").write_text("one\ntwo\nthree\n")
10
+ chaos.script("sed -n '1,2p f.txt'", ("done", "ok"))
11
+ r = tether.run("show the first lines of f.txt", "--tools", "bash")
12
+ out = tether.tool_outputs(r["id"])[0]
13
+ assert "one" in out and "two" in out
14
+ assert "extra characters" not in out
@@ -0,0 +1,18 @@
1
+ """F2: a fetch with #anchor and --from that cannot run is reported, not crashed.
2
+
3
+ Scripted: replies are fixed, so the result never depends on the model.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from e2e.conftest import Tether
9
+
10
+
11
+ def test_bad_fetch_with_anchor_and_from_reports_error(tether: Tether, chaos) -> None:
12
+ # ftp is refused before any network I/O, so the scenario is deterministic offline
13
+ fetch = "web fetch ftp://example.org/page#top --from intro"
14
+ chaos.script((fetch, None), ("done", "stopped"))
15
+ r = tether.run("fetch", "--tools", "web", "--max-steps", "5")
16
+ assert r["rc"] == 0, r
17
+ outputs = tether.tool_outputs(r["id"])
18
+ assert any("URL must start with http:// or https://" in o for o in outputs), outputs
@@ -0,0 +1,31 @@
1
+ """#283: a headless worker with empty and relative PYTHONPATH entries still runs (scripted)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+
8
+ from e2e.conftest import Tether
9
+
10
+
11
+ def _cli(t: Tether, env: dict[str, str], *args: str) -> subprocess.CompletedProcess[str]:
12
+ return subprocess.run(
13
+ [sys.executable, "-P", "-m", "tether.cli", *args],
14
+ cwd=t.ws,
15
+ env=env,
16
+ capture_output=True,
17
+ text=True,
18
+ timeout=120,
19
+ )
20
+
21
+
22
+ def test_headless_run_survives_empty_and_relative_pythonpath(tether: Tether, chaos) -> None:
23
+ chaos.script(("done", "ok"))
24
+ dirty = dict(tether.env)
25
+ dirty["PYTHONPATH"] = ":."
26
+ p = _cli(tether, dirty, "run", "--headless", "--model", "e2e/scripted", "go")
27
+ sid = p.stdout.strip()
28
+ assert p.returncode == 0 and sid.startswith("ses_"), p.stderr
29
+ assert _cli(tether, tether.env, "wait", sid).returncode == 0
30
+ r = _cli(tether, tether.env, "read", sid)
31
+ assert "ok" in r.stdout, r.stdout + r.stderr
@@ -98,6 +98,7 @@ nav:
98
98
  - A pipeline where each stage has its own job: guides/a-pipeline-with-checks.md
99
99
  - Give the model a tool of your own: guides/add-your-own-tool.md
100
100
  - Run a session on its own branch: guides/own-branch.md
101
+ - Write tasks a small model can finish: guides/small-models.md
101
102
  - tutorial/index.md
102
103
  - CLI and Headless Sessions: tutorial/cli-and-headless.md
103
104
  - Python SDK and Custom Tools: tutorial/sdk-and-custom-tools.md