tether-sdk 0.2.16__tar.gz → 0.2.18__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 (272) hide show
  1. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/AGENTS.md +12 -9
  2. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/PKG-INFO +2 -2
  3. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/README.md +1 -1
  4. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/architecture.md +18 -0
  5. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/agent-interface.md +1 -1
  6. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/context-and-store.md +8 -0
  7. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/tools.md +2 -0
  8. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/release-notes.md +37 -0
  9. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/troubleshooting.md +1 -1
  10. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/built-in-tools.md +6 -1
  11. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/workflows-and-telemetry.md +1 -1
  12. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_agents.py +12 -0
  13. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_progress.py +16 -2
  14. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_quirks.py +41 -0
  15. tether_sdk-0.2.18/e2e/test_restart.py +65 -0
  16. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/coding_edit_failure_modes.snap.json +10 -10
  17. tether_sdk-0.2.18/tests/test_auto_test.py +90 -0
  18. tether_sdk-0.2.18/tests/test_edit_only_shell.py +71 -0
  19. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_edit_tool.py +14 -0
  20. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_multiplexer.py +24 -0
  21. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_pipeline.py +47 -0
  22. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_progress.py +3 -1
  23. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_restart.py +83 -10
  24. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_store.py +6 -3
  25. tether_sdk-0.2.18/tests/test_test_reasoning.py +105 -0
  26. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_tether.py +1 -1
  27. tether_sdk-0.2.18/tests/test_trace.py +142 -0
  28. tether_sdk-0.2.18/tests/tools/test_bash.py +59 -0
  29. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/__init__.py +1 -1
  30. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/cli.py +2 -0
  31. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/context.py +23 -2
  32. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/harness.py +233 -22
  33. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/multiplexer.py +44 -0
  34. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/store.py +7 -3
  35. tether_sdk-0.2.18/tether/tools/bash.py +75 -0
  36. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/tools/edit.py +20 -4
  37. tether_sdk-0.2.18/tether/trace.py +142 -0
  38. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/unchanged.py +3 -3
  39. tether_sdk-0.2.16/e2e/test_restart.py +0 -37
  40. tether_sdk-0.2.16/tests/tools/test_bash.py +0 -19
  41. tether_sdk-0.2.16/tether/tools/bash.py +0 -30
  42. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/active/.gitkeep +0 -0
  43. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/active/north-star.md +0 -0
  44. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/active/run-progress.md +0 -0
  45. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/active/why-qwen-fails.md +0 -0
  46. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/archive/.gitkeep +0 -0
  47. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/archive/v0-2-bugs.md +0 -0
  48. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/archive/work-on-v0-2-4.md +0 -0
  49. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/why-qwen-fails/reg_ab.sh +0 -0
  50. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/why-qwen-fails/regcount.py +0 -0
  51. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru/why-qwen-fails/whack.py +0 -0
  52. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.claude/gru.json +0 -0
  53. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.github/workflows/ci.yml +0 -0
  54. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.github/workflows/delivery.yml +0 -0
  55. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.github/workflows/docs.yml +0 -0
  56. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/.gitignore +0 -0
  57. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/CONTRIBUTING.md +0 -0
  58. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/LICENSE +0 -0
  59. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/README.md +0 -0
  60. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/canary.py +0 -0
  61. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/canary_baseline.json +0 -0
  62. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/compare.py +0 -0
  63. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/grade.py +0 -0
  64. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/harness-bench/README.md +0 -0
  65. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/harness-bench/tether-adapter.patch +0 -0
  66. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/history.jsonl +0 -0
  67. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/base.py +0 -0
  68. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/baseline/harness.jsonl +0 -0
  69. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/baseline/ladder.jsonl +0 -0
  70. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/charts.py +0 -0
  71. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/general.py +0 -0
  72. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/probe.py +0 -0
  73. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/refs.py +0 -0
  74. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/report.py +0 -0
  75. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/run.py +0 -0
  76. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/snapshot.py +0 -0
  77. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/tasks.py +0 -0
  78. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/ladder/validate.py +0 -0
  79. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/little_coder_recipe.md +0 -0
  80. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/naive_agent.py +0 -0
  81. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/report.py +0 -0
  82. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/run.py +0 -0
  83. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/goal.txt +0 -0
  84. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/hidden/test_hidden.py +0 -0
  85. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ref/calc/__init__.py +0 -0
  86. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ref/calc/errors.py +0 -0
  87. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ref/calc/evaluator.py +0 -0
  88. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ref/calc/parser.py +0 -0
  89. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ref/calc/tokenizer.py +0 -0
  90. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/calc/__init__.py +0 -0
  91. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/calc/errors.py +0 -0
  92. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/calc/evaluator.py +0 -0
  93. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/calc/parser.py +0 -0
  94. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/calc/tokenizer.py +0 -0
  95. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/tests/__init__.py +0 -0
  96. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-hard/ws/tests/test_calc.py +0 -0
  97. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/goal.txt +0 -0
  98. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/hidden/test_hidden.py +0 -0
  99. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/cli.py +0 -0
  100. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/ledger/__init__.py +0 -0
  101. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/ledger/parser.py +0 -0
  102. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/ledger/report.py +0 -0
  103. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/sample.csv +0 -0
  104. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/tests/__init__.py +0 -0
  105. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-medium/ws/tests/test_report.py +0 -0
  106. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-simple/goal.txt +0 -0
  107. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-simple/hidden/test_hidden.py +0 -0
  108. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-simple/ws/tests/__init__.py +0 -0
  109. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-simple/ws/tests/test_textutils.py +0 -0
  110. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/code-simple/ws/textutils.py +0 -0
  111. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/codereview/goal.txt +0 -0
  112. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/codereview/ws/inventory.py +0 -0
  113. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/content/goal.txt +0 -0
  114. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/content/ws/.keep +0 -0
  115. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/goal.txt +0 -0
  116. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/hidden/test_hidden.py +0 -0
  117. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/AGENTS.md +0 -0
  118. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/README.md +0 -0
  119. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/docs/architecture.md +0 -0
  120. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/docs/style.md +0 -0
  121. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/mini/__init__.py +0 -0
  122. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/mini/cli.py +0 -0
  123. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/mini/fmt.py +0 -0
  124. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/mini/stats.py +0 -0
  125. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/mini/textio.py +0 -0
  126. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/tests/__init__.py +0 -0
  127. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/focus/ws/tests/test_stats.py +0 -0
  128. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/plan/goal.txt +0 -0
  129. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/plan/ws/README.md +0 -0
  130. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/plan/ws/svc/__init__.py +0 -0
  131. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/plan/ws/svc/client.py +0 -0
  132. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/plan/ws/svc/config.py +0 -0
  133. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/qa/goal.txt +0 -0
  134. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/qa/ws/.keep +0 -0
  135. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/review/goal.txt +0 -0
  136. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/bench/tasks/review/ws/paper.md +0 -0
  137. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/benchmarks.md +0 -0
  138. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/extending.md +0 -0
  139. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/glossary.md +0 -0
  140. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/guides/a-pipeline-with-checks.md +0 -0
  141. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/guides/add-your-own-tool.md +0 -0
  142. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/guides/fix-a-failing-test.md +0 -0
  143. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/guides/use-tether-from-your-agent.md +0 -0
  144. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/index.md +0 -0
  145. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/javascripts/mathjax.js +0 -0
  146. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/claude-haiku.md +0 -0
  147. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/general.md +0 -0
  148. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/img/harness-cost.svg +0 -0
  149. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/img/harness-pass.svg +0 -0
  150. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/img/task-grid.svg +0 -0
  151. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/img/tiers.svg +0 -0
  152. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/index.md +0 -0
  153. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/ladder/results.md +0 -0
  154. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/llms.txt +0 -0
  155. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/cli.md +0 -0
  156. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/harness.md +0 -0
  157. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/index.md +0 -0
  158. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/multiplexer-and-decorator.md +0 -0
  159. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/providers.md +0 -0
  160. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/reference/sandbox-and-skills.md +0 -0
  161. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/agent-interface.md +0 -0
  162. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/cli-and-headless.md +0 -0
  163. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/index.md +0 -0
  164. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/multi-agent.md +0 -0
  165. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/sdk-and-custom-tools.md +0 -0
  166. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/security-and-sandbox.md +0 -0
  167. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/docs/tutorial/skills.md +0 -0
  168. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/__init__.py +0 -0
  169. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/conftest.py +0 -0
  170. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/proxy.py +0 -0
  171. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_defaults.py +0 -0
  172. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_faults.py +0 -0
  173. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_lifecycle.py +0 -0
  174. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_limits.py +0 -0
  175. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_notes.py +0 -0
  176. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_sandbox.py +0 -0
  177. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/e2e/test_surface.py +0 -0
  178. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/README.md +0 -0
  179. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/checks.py +0 -0
  180. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/course.yaml +0 -0
  181. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/ruff.toml +0 -0
  182. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/skills/planner/SKILL.md +0 -0
  183. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/skills/searcher/SKILL.md +0 -0
  184. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/skills/searcher/tools.py +0 -0
  185. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/skills/writer/SKILL.md +0 -0
  186. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/examples/course/skills/writer/tools.py +0 -0
  187. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/mkdocs.yml +0 -0
  188. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/pyproject.toml +0 -0
  189. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/scripts/record_golden.py +0 -0
  190. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/skills/researcher/SKILL.md +0 -0
  191. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/__init__.py +0 -0
  192. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/conftest.py +0 -0
  193. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/fixtures/dialects.jsonl +0 -0
  194. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/coding_edit_failure_modes.json +0 -0
  195. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/done_nudge.json +0 -0
  196. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/done_nudge.snap.json +0 -0
  197. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/read_range_pingpong.json +0 -0
  198. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/read_range_pingpong.snap.json +0 -0
  199. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/research_web.json +0 -0
  200. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/research_web.snap.json +0 -0
  201. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/shell_wrappers.json +0 -0
  202. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/shell_wrappers.snap.json +0 -0
  203. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/waiting_input_resume.json +0 -0
  204. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/golden/waiting_input_resume.snap.json +0 -0
  205. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/__init__.py +0 -0
  206. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_anthropic.py +0 -0
  207. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_base.py +0 -0
  208. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_declarative.py +0 -0
  209. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_dialects.py +0 -0
  210. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_ollama.py +0 -0
  211. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_openai.py +0 -0
  212. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/providers/test_retry.py +0 -0
  213. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_agent_interface.py +0 -0
  214. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_bench_littlecoder.py +0 -0
  215. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_benchmarks_docs.py +0 -0
  216. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_cli.py +0 -0
  217. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_cli_docs.py +0 -0
  218. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_context.py +0 -0
  219. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_context_config.py +0 -0
  220. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_decorator.py +0 -0
  221. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_docs_structure.py +0 -0
  222. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_failing_test_source.py +0 -0
  223. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_golden.py +0 -0
  224. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_guides_docs.py +0 -0
  225. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_harness.py +0 -0
  226. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_integration.py +0 -0
  227. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_ladder_general.py +0 -0
  228. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_ladder_run.py +0 -0
  229. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_llms_txt.py +0 -0
  230. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_misses.py +0 -0
  231. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_params.py +0 -0
  232. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_providers_docs.py +0 -0
  233. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_quiet_and_fold.py +0 -0
  234. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_readme.py +0 -0
  235. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_regression_note.py +0 -0
  236. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_release_hardening.py +0 -0
  237. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_repeat_reads.py +0 -0
  238. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_retro.py +0 -0
  239. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_sandbox.py +0 -0
  240. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_security_hardening.py +0 -0
  241. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_skills.py +0 -0
  242. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_small_rewrite.py +0 -0
  243. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_structured_history.py +0 -0
  244. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_troubleshooting_docs.py +0 -0
  245. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/test_unchanged_write.py +0 -0
  246. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/tools/__init__.py +0 -0
  247. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/tools/test_agent.py +0 -0
  248. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/tools/test_done.py +0 -0
  249. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/tools/test_edit.py +0 -0
  250. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/tools/test_mem.py +0 -0
  251. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tests/tools/test_web.py +0 -0
  252. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/api.py +0 -0
  253. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/decorator.py +0 -0
  254. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/mcp.py +0 -0
  255. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/pipeline.py +0 -0
  256. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/progress.py +0 -0
  257. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/providers/__init__.py +0 -0
  258. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/providers/anthropic.py +0 -0
  259. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/providers/base.py +0 -0
  260. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/providers/declarative.py +0 -0
  261. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/providers/ollama.py +0 -0
  262. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/providers/openai.py +0 -0
  263. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/retro.py +0 -0
  264. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/sandbox.py +0 -0
  265. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/schema.py +0 -0
  266. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/skills.py +0 -0
  267. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/tools/__init__.py +0 -0
  268. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/tools/agent.py +0 -0
  269. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/tools/done.py +0 -0
  270. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/tools/mem.py +0 -0
  271. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/tether/tools/web.py +0 -0
  272. {tether_sdk-0.2.16 → tether_sdk-0.2.18}/uv.lock +0 -0
@@ -29,10 +29,10 @@ Tether stays **lightweight, reliable, extensible, model-agnostic, easy to use, r
29
29
  Reviewers and the Team Leader cite the failing line when they reject a change.
30
30
 
31
31
  ### North Star (the one number; see tracker #173)
32
- **Polyglot-Python pass rate on `ollama/qwen3.5:9b` (our weakest model), default config, full 34 exercises: 80% by v0.3.0.** Nothing important is set aside for it, but everything is ranked by it:
32
+ **Polyglot-Python pass rate on `ollama/qwen3.5:9b` (our weakest model), default config, full 34 exercises x 2 runs: 74% (50 of 68) by v0.4.0.** 74% is what the model can already do: 25 of 34 exercises have passed at least once from scratch, so the goal is that each of them passes every run (reliability), not new capability. 9 exercises never passed from scratch (bowling, connect, forth, hangman, react, rest-api, scale-generator, sgf-parsing, transpose); beating 74% needs them, and is not planned for. Nothing important is set aside for it, but everything is ranked by it:
33
33
  1. Every issue, PR and benchmark must say how it moves this number (or tokens per solved exercise at the same pass rate). A benchmark that cannot show a change in it is not run; an issue that cannot name the link waits behind one that can.
34
- 1b. Two lanes, both tracked: the **general-purpose default** (no role) is the official number and must reach 80%; the shipped **`swe` role** (#172) is reported next to it, must never score below the default, and gets its own target once its baseline is pinned.
35
- 2. Weakest-model rule: every stronger model must score at least what `qwen3.5:9b` scores (and at least 80% once reached). If we plateau, stronger models may not fall below the plateau.
34
+ 1b. Two lanes, both tracked: the **general-purpose default** (no role) is the official number and must reach 74%; the shipped **`swe` role** (#172) is reported next to it, must never score below the default, and gets its own target once its baseline is pinned.
35
+ 2. Weakest-model rule: every stronger model must score at least what `qwen3.5:9b` scores (and at least 74% once reached). If we plateau, stronger models may not fall below the plateau.
36
36
  3. Other indicators (tokens per pass, steps, wall-clock, prompt size, coverage) may slow or pause but never degrade between releases.
37
37
  4. Floor, a promise rather than a goal: every task delivers a real, honest, usable result in a fair time; no unretried outage, no empty result, no unrelated hallucinated deliverable (#171).
38
38
  5. Every PR body and issue-close comment ends with `north star: <before> -> <after> (runs, noise, run dir)`; the tracker's History table and `bench/history.jsonl` are updated at every release.
@@ -54,7 +54,7 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
54
54
  - Sessions do **not** terminate simply because the model emitted a text response without a tool call (small 9B models frequently "think out loud" before acting).
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
  - 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.
57
- - 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). After the same check command (a test run) has failed 3 times with an edit in between, the harness adds one `restart` turn (event_type `restart`, at most 6000 chars: the files as the model left them, the last failing output, and counts of what was tried) and continues from that turn alone; failed commands are not replayed, at most one restart per session, and everything before it stays in SQLite (0 static prompt tokens, no extra model call).
57
+ - 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
58
  - 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. 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).
59
59
 
60
60
  ### Rule 4: Progressive `--help` (JIT Schema Disclosure)
@@ -71,8 +71,10 @@ Reviewers and the Team Leader cite the failing line when they reject a change.
71
71
  ```text
72
72
  [obs_3 folded: `web fetch https://...` (3,420 chars) | use `mem recall obs_3` to expand]
73
73
  ```
74
- - 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|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").
74
+ - 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").
75
+ - 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).
75
76
  - 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).
77
+ - 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`.
76
78
  - 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.
77
79
 
78
80
  ### Rule 6: Deterministic Security Envelope (Never Trust the LLM for Safety)
@@ -107,6 +109,7 @@ tether/
107
109
  ├── multiplexer.py # Action Plane: `act(cmd, stdin)` parser, command registry, JIT --help
108
110
  ├── decorator.py # `@tool` introspection: converts Python type hints + docstrings to CLI specs
109
111
  ├── context.py # State Plane: ContextAssembler, EphemeralFolder, token estimator
112
+ ├── trace.py # Runtime trace of the smallest failing test (calls, arguments, results)
110
113
  ├── skills.py # Skill loader: Markdown + YAML frontmatter + optional sibling `tools.py` (its tools must be listed in `tools:`)
111
114
  ├── providers/
112
115
  │ ├── base.py # Abstract async ModelProvider protocol (`generate(messages, tools)`)
@@ -117,7 +120,7 @@ tether/
117
120
  ├── done.py # Mandatory explicit completion command (`done`)
118
121
  ├── bash.py # Kernel-sandboxed, env-scrubbed, workspace-bounded shell execution
119
122
  ├── web.py # Zero-config DuckDuckGo Lite httpx search (+ Brave/Tavily/SearXNG env override) & reader
120
- ├── edit.py # Workspace-jailed file viewing (`view`) & stdin patching (`write`, `replace`)
123
+ ├── edit.py # Workspace-jailed file viewing (`view`) & stdin patching (`write`, `append`, `replace`)
121
124
  ├── mem.py # Working scratchpad (`pin`/`unpin`), FTS5 search (`search`), & recall (`recall`)
122
125
  └── agent.py # Async multi-agent orchestration (`spawn`, `status`, `wait`, `query`, `read`, `send`)
123
126
  ```
@@ -133,9 +136,9 @@ tether/
133
136
  - `chunks_fts` *(SQLite FTS5 Virtual Table)*: `session_id`, `source_type` (`result` | `observation` | `pin`), `source_id`, `chunk_index`, `content` — automatically indexes ~150-token paragraph/heading chunks of all observations, pins, and final `done` deliverables for sub-millisecond BM25 RAG (including cross-session project search via `mem search --project <query>`).
134
137
  - `pins`: `session_id`, `key`, `value` (supports session-scoped working memory and workspace-wide semantic facts under `GLOBAL_SESSION_ID = "__project__"` via `mem pin --project <key> <val>`).
135
138
  - `inbox`: `id`, `session_id`, `message`, `consumed` (`0` or `1`), `created_at`.
136
- - **Agent-to-Agent Interface (`api.py`, `mcp.py`):** `--json` outputs (schema `v: 1`), exit codes `0` completed / `1` usage / `2` waiting (also stuck or out of steps, handed back) / `3` gate / `5` worker error, failure text on stderr, `--result-file`, `--output-schema`, `tether events`. `tether mcp` serves the same JSON over stdio and requires the optional `tether-sdk[mcp]` extra (official `mcp` SDK, imported lazily so core stays dependency-light; Rule 7 allows it as an extra only); its sandbox is always rooted at `--workspace` and callers can only narrow it. Details: `docs/reference/agent-interface.md`.
139
+ - **Agent-to-Agent Interface (`api.py`, `mcp.py`):** `--json` outputs (schema `v: 1`), exit codes `0` completed / `1` usage / `2` waiting (also stuck or out of steps, handed back) / `3` gate / `5` worker error, failure text on stderr, a foreground `run` writes `session ses_<id>` to stderr at creation (#90), `--result-file`, `--output-schema`, `tether events`. `tether mcp` serves the same JSON over stdio and requires the optional `tether-sdk[mcp]` extra (official `mcp` SDK, imported lazily so core stays dependency-light; Rule 7 allows it as an extra only); its sandbox is always rooted at `--workspace` and callers can only narrow it. Details: `docs/reference/agent-interface.md`.
137
140
  - **Telemetry & Failure-Mode Inspector (`tether stats`):**
138
- - `tether stats [--model <m>] [--skill <s>] [--limit <n>]` aggregates per-model pass rates, cumulative token economy (`total_prompt_tokens + total_completion_tokens`), step latency, and self-healing ACI friction (`nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`), plus a top-offenders breakdown by tool command.
141
+ - `tether stats [--model <m>] [--skill <s>] [--limit <n>]` aggregates per-model pass rates, cumulative token economy (`total_prompt_tokens + total_completion_tokens`), step latency, and self-healing ACI friction (`nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`), plus a top-offenders breakdown by tool command (a leading `cd <dir> &&` is skipped; no command is `(no command)`, #79).
139
142
  - **Post-Mortem Distillation (`tether retro <session_id> [--apply] [--model <m>]`):** offline only (never mutates a live prompt). `store.extract_friction_trace` pulls `inbox` corrections and `loop_stall`/`cycle_stall`/`syntax_error`/`tool_error`/`gate_fail`/`gate_error` turns (+ the fixing command); one bounded LLM pass yields ≤2 rules (≤30 tokens each, deduped against existing rules). Prints a unified diff of `SKILL.md`; `--apply` appends under `## Learned Rules` (or pins to project memory when no skill was used).
140
143
  - **Headless Worker & Live `send` Contract:**
141
144
  - `max_steps` is the absolute session ceiling. `send` on a session that used its steps extends it to `step_count + <Harness budget>` (a session paused earlier keeps its remaining budget), so `step_count` never exceeds `max_steps` (#75).
@@ -216,7 +219,7 @@ uv run mypy tether/
216
219
  2. **Async-First Design:**
217
220
  - All model calls and I/O-bound tool executions must be `async`/`await` native, while seamlessly wrapping synchronous user `@tool` functions.
218
221
  3. **Graceful Degradation on 9B Hallucinations:**
219
- - 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 `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), 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.
222
+ - 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.
220
223
  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`.
221
224
  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`.
222
225
  6. **Documentation Integrity:**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tether-sdk
3
- Version: 0.2.16
3
+ Version: 0.2.18
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
@@ -53,7 +53,7 @@ Tether answers to one number: how many real coding exercises a small local model
53
53
  | Mean score | 0.718 | 0.763 | 0.783 | 0.756 |
54
54
  | Runs killed by the wall cap | 11 | 14 | 13 | 12 |
55
55
 
56
- The target is 80% by v0.3.0, so this is the number every change is ranked by. v0.2.15 passes 3 more runs than v0.2.13, but that is inside the noise too (pass flips +7/-4, paired score delta -0.026, 95% interval -0.093 to +0.041). One run is noisy (about 0.1), so read the pass count and the failure breakdown, not one decimal. The second yardstick is the [Coding Ladder](https://igormcsouza.github.io/tether/ladder/): 24 tasks, 6 tiers of difficulty times write, bugfix, plan and review, which shows how far up a model climbs and what stops it. Method, how to reproduce both and fair-comparison rules: [Benchmarks](https://igormcsouza.github.io/tether/benchmarks/).
56
+ The target is 74% (50 of 68) by v0.4.0, so this is the number every change is ranked by. It is what qwen3.5:9b can already do: 25 of the 34 exercises have passed at least once from scratch, so the work is making each of them pass every run. For comparison, the best published harness for this model, [little-coder](https://github.com/itayinbarr/little-coder), reports 52.9% on the same 34 Python exercises (its own setup). v0.2.15 passes 3 more runs than v0.2.13, but that is inside the noise too (pass flips +7/-4, paired score delta -0.026, 95% interval -0.093 to +0.041). One run is noisy (about 0.1), so read the pass count and the failure breakdown, not one decimal. The second yardstick is the [Coding Ladder](https://igormcsouza.github.io/tether/ladder/): 24 tasks, 6 tiers of difficulty times write, bugfix, plan and review, which shows how far up a model climbs and what stops it. Method, how to reproduce both and fair-comparison rules: [Benchmarks](https://igormcsouza.github.io/tether/benchmarks/).
57
57
 
58
58
  ## How it works
59
59
 
@@ -32,7 +32,7 @@ Tether answers to one number: how many real coding exercises a small local model
32
32
  | Mean score | 0.718 | 0.763 | 0.783 | 0.756 |
33
33
  | Runs killed by the wall cap | 11 | 14 | 13 | 12 |
34
34
 
35
- The target is 80% by v0.3.0, so this is the number every change is ranked by. v0.2.15 passes 3 more runs than v0.2.13, but that is inside the noise too (pass flips +7/-4, paired score delta -0.026, 95% interval -0.093 to +0.041). One run is noisy (about 0.1), so read the pass count and the failure breakdown, not one decimal. The second yardstick is the [Coding Ladder](https://igormcsouza.github.io/tether/ladder/): 24 tasks, 6 tiers of difficulty times write, bugfix, plan and review, which shows how far up a model climbs and what stops it. Method, how to reproduce both and fair-comparison rules: [Benchmarks](https://igormcsouza.github.io/tether/benchmarks/).
35
+ The target is 74% (50 of 68) by v0.4.0, so this is the number every change is ranked by. It is what qwen3.5:9b can already do: 25 of the 34 exercises have passed at least once from scratch, so the work is making each of them pass every run. For comparison, the best published harness for this model, [little-coder](https://github.com/itayinbarr/little-coder), reports 52.9% on the same 34 Python exercises (its own setup). v0.2.15 passes 3 more runs than v0.2.13, but that is inside the noise too (pass flips +7/-4, paired score delta -0.026, 95% interval -0.093 to +0.041). One run is noisy (about 0.1), so read the pass count and the failure breakdown, not one decimal. The second yardstick is the [Coding Ladder](https://igormcsouza.github.io/tether/ladder/): 24 tasks, 6 tiers of difficulty times write, bugfix, plan and review, which shows how far up a model climbs and what stops it. Method, how to reproduce both and fair-comparison rules: [Benchmarks](https://igormcsouza.github.io/tether/benchmarks/).
36
36
 
37
37
  ## How it works
38
38
 
@@ -102,6 +102,24 @@ Iterative LLM repair often breaks code that already worked: across six models, 4
102
102
 
103
103
  0 static prompt tokens, no model call, at most 400 chars; nothing is added when no test broke.
104
104
 
105
+ ### The Repair Loop Around Tests — *Ridnik et al. (AlphaCodium, 2024), Zhong et al. (LDB, ACL 2024), Olausson et al. (ICLR 2024), Aider auto-test*
106
+ On a small local model most of the time goes into typing code: a whole-file rewrite of a 100-line module costs `qwen3.5:9b` 30 to 100 seconds, while running the tests takes under one. So the harness spends its own (free) time around each test run, and never the model's:
107
+
108
+ - **Tests after every edit.** 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 under `[auto-test]`. The model sees whether a version works before it types the next one.
109
+ - **Say what the tests expect, first.** When the model reads the test file of a file it is about to write (`paasio_test.py` next to `paasio.py`), one `[BEFORE WRITING]` line asks it to state in plain words what the 3 hardest tests call and expect, then write. Reasoning about the tests before the code raised GPT-4 from 19% to 44% on CodeContests in AlphaCodium.
110
+ - **Show what the code did.** When the same tests fail again after a change, or only a few still fail, the harness runs the smallest failing test once more under a tracer and appends the calls into workspace code, with their arguments and results (2 levels deep, at most 30 lines):
111
+
112
+ ```text
113
+ [runtime trace of test_f: calls into your code, arguments and results]
114
+ test_f(self=<m_checks.T testMethod=test_f>)
115
+ f()
116
+ -> 1
117
+ raised AssertionError: 1 != 3
118
+ ```
119
+ - **Start fresh, or keep the near pass.** When the same check fails 3 times with edits between, the files the model changed go back to how they started and the history restarts from a short description that names the failing tests, not the failed code: for weaker models a new attempt beats repairing a wrong one (Olausson et al.). A version that already passes all but at most 3 tests is kept instead: when later edits do not beat it, its files come back in place with one `[RESTORED]` line and the history stays, so the model does not spend its last minutes re-reading the tests.
120
+
121
+ 0 static prompt tokens and no extra model call for any of these. On the `paasio` exercise with `qwen3.5:9b` (600 s cap), these moved the score from a mean of 0.63 over 3 runs to 0.94 over 5 runs, with 1 of 5 passing; the rest ended at the time cap with 22 to 24 of 25 tests passing.
122
+
105
123
  ### Naming a Rewrite That Changed Nothing — *Wang-Lin et al. (2026)*
106
124
  The same repair cycles show up in their smallest form: a model rewrites a whole file with the code it already had, sometimes byte for byte, sometimes with only a comment changed, then runs the tests and gets the same failure. In one 8-exercise run of `qwen3.5:9b`, 9 of 62 rewrites were like this. Before a command that writes a file that already exists (`edit write|replace <p>`, `> p`, `>> p`, `tee p`), the harness keeps its bytes; after the command, when every such file holds the same code, it appends one line:
107
125
 
@@ -17,7 +17,7 @@ Machine-readable surface for calling Tether from another agent. Setup per harnes
17
17
  - `failure` is `{"kind": "gate|stopped|worker_error", "gate": "<cmd>|output-schema|null", "message": "…"}`.
18
18
  - Keys are stable; new keys may be added.
19
19
 
20
- `--json` is available on `run`, `pipeline`, `status`, `list`, `read` (also `--digest`, `--steps`, `--full`), `wait`, `send` and `stats`. With `--headless` it prints `{"v":1,"id":"ses_…"}`.
20
+ `--json` is available on `run`, `pipeline`, `status`, `list`, `read` (also `--digest`, `--steps`, `--full`), `wait`, `send` and `stats`. With `--headless` it prints `{"v":1,"id":"ses_…"}`. A foreground `run` (with or without `--json`) writes `session ses_…` to stderr as soon as the session exists, before the first model call, so a caller that kills it on its own timeout can still `status`, `stop` or `send` it; stdout is unchanged.
21
21
 
22
22
  ## Exit codes
23
23
 
@@ -53,3 +53,11 @@ Re-read the failing output above before you change anything.
53
53
  ```
54
54
 
55
55
  The hand-back has the same body under the header `[STUCK at step 31 after 2 restarts. Handing back to you.]`, and ends with "The work is not progressing. Refine the request or give guidance, then send it again."
56
+
57
+ When the restart comes from a failing check, it is a fresh start: the files the model changed go back to how they were when the session started (files it created stay), the message lists the tests that kept failing instead of the failed output, and asks for a different approach. A run that passes all but at most 3 tests (and more than twice as many as fail) is a near pass and is kept: when 3 later runs do not beat it, its files are put back in place with one line, and there is no restart, so the history stays:
58
+
59
+ ```text
60
+ [RESTORED] Your last 3 changes did not beat your best version, so its files are back. It failed only: test_meteredfile_iteration. Fix those from there.
61
+ ```
62
+
63
+ After the first test run, edits are followed by an automatic test run (`[auto-test]`), and those runs count toward the 3 like the model's own. See "The Repair Loop Around Tests" in [Architecture](../architecture.md).
@@ -16,6 +16,8 @@
16
16
 
17
17
  ## `tether.tools.edit`
18
18
 
19
+ With `edit` enabled and `bash` not, a plain shell file write or read runs as `edit`: `echo ok > f`, `printf 'a\nb' > f` and `cat > f << 'EOF' ... EOF` as `edit write f`, `cat f` as `edit view f`, and `>> f` as `edit append f`. `edit append <path>` adds `stdin` to the end of a file (created if missing); `edit write` replaces it. Other shell commands answer `unknown command` with a pointer to `edit`.
20
+
19
21
  ::: tether.tools.edit
20
22
  options:
21
23
  show_root_heading: false
@@ -1,5 +1,42 @@
1
1
  # Release Notes
2
2
 
3
+ ## Unreleased
4
+
5
+ ## v0.2.18 (2026-11-08)
6
+
7
+ Small fixes from the v0.2.17 release check and the paasio labs: shell-style file writes work when only `edit` is enabled, `edit append` adds to a file instead of replacing it, a foreground run prints its session id at once, and a `cd` into a container path that does not exist no longer costs a step.
8
+
9
+ ### Added
10
+
11
+ - `edit append <path>` adds `stdin` to the end of a file (created if missing, with a newline added when the file does not end with one). Small models assumed `edit write` appends: gemma4:e4b wrote a plan module by module and kept only the last one. The `edit` help now says `write replaces the file` and stays within 59 tokens; with `bash` off, `echo t >> f` runs as `edit append f` (#34).
12
+ - A foreground `tether run` (with or without `--json`) writes `session ses_<id>` to stderr as soon as the session is created, before the first model call. A caller that kills the run on its own timeout no longer needs `tether list` to find the id for `status`, `stop` or `send`; stdout is unchanged (#90).
13
+
14
+ ### Fixed
15
+
16
+ - `cd /testbed && python3 -m unittest ...`, with a directory that does not exist, now runs the command after the `cd` in the workspace, with one line saying the `cd` was skipped. Models assume a container path; qwen3.5:9b lost a step to it in 3 of the last 6 paasio runs. A `cd` into a real directory runs as before (#79).
17
+ - `tether stats` offenders: a leading `cd <dir> &&` no longer makes `cd` the offending command (the command after it is counted), and a turn without a command shows as `(no command)` instead of `unknown` (#79).
18
+ - With `edit` enabled and `bash` not, a plain shell file write or read now runs as `edit`: `echo ok > hello.txt`, `printf 'a\nb' > f`, `cat > f << 'EOF' ... EOF` become `edit write`, and `cat f` becomes `edit view f`. Any other shell command still answers `unknown command`, now with a pointer to `edit view|write`. In the v0.2.17 release check (`--tools edit`, "Create hello.txt containing ok"), qwen3.5:9b tried `echo`, `printf`, `cat` and `ls` 10 times before writing the file at step 12 of 15. Pipes are not translated.
19
+
20
+ ## v0.2.17 (2026-11-07)
21
+
22
+ When tests fail, tether now works around them on its own time: it re-runs them after each edit, traces the smallest failing test, starts fresh when an approach keeps failing, and keeps a version that nearly passes instead of throwing it away. The north star (74% on Polyglot-Python with qwen3.5:9b) moves to v0.4.0. No full Polyglot run was made for this release; the numbers below are from targeted labs.
23
+
24
+ ### Added
25
+
26
+ - A repair loop around failing tests, all from the harness's own time (0 static prompt tokens, no extra model call). Once the model has run the tests, they run again after each edit and the result is appended (`[auto-test]`), so no version is typed blind. When the model reads the tests of the file it is about to write, it is asked once to say in plain words what the hardest tests call and expect (`[BEFORE WRITING]`). When the same tests fail again after a change, or only a few still fail, a runtime trace of the smallest failing test shows the calls into your code with their arguments and results. When at most 3 tests still fail, one note points to `edit replace` instead of a whole-file rewrite. On `paasio` with qwen3.5:9b (600 s cap) the mean score went from 0.63 over 3 runs to 0.94 over 5 runs, with 1 of 5 passing; the other 4 ended at the time cap with 22 to 24 of 25 tests passing (#173).
27
+
28
+ ### Changed
29
+
30
+ - The north star is now 74% (50 of 68) on Polyglot-Python with qwen3.5:9b by v0.4.0, instead of 80% by v0.3.0. 74% is the share of exercises the model has already solved at least once from scratch (25 of 34), so the target is reliability: each of them passing every run. The 9 exercises it never solved are out of scope for this target (#173).
31
+ - A restart after the same tests failed 3 times is now a fresh start: the files the model changed go back to how they started, and the restart message names the failing tests instead of quoting the failed code (Olausson et al., ICLR 2024). A version that already passes all but at most 3 tests is kept instead: when later edits do not beat it, its files come back in place with a `[RESTORED]` line and the history stays (#173).
32
+ - The number of failing tests is read from the runner's summary line (`FAILED (failures=1, errors=3)`), so a test run cut by `| head` is no longer taken for a near pass (#173).
33
+
34
+ ### Fixed
35
+
36
+ - `edit write hello.txt ok`, with the content typed after the path and no `stdin`, now writes `ok` instead of answering ``Error: `edit write` requires file content in `stdin` ``. qwen3.5:9b lost two steps this way creating a one-line file in the v0.2.16 smoke run. The text is taken as typed (spacing kept, one pair of surrounding quotes removed, literal `\n` unescaped); `stdin`, when given, still wins (#173).
37
+ - In `tether list`, a pipeline's own row now shows the model its stages run on instead of the harness default (`ollama/qwen3.5:9b` even when every stage set `model: local/qwen3.5:9b`). When stages use different models, the row lists them in stage order, joined with `,`. A stage with no `model:` still runs on the pipeline's default model.
38
+ - A test run piped to `head` (`python3 -m unittest ... 2>&1 | head -100`) now keeps its result. The runner prints its summary (`Ran 26 tests` / `FAILED (failures=8)`) last, so `head` cut it, and the pipe also replaced the tests' exit code with `head`'s 0: the model could not tell how many tests failed, and tether's own notes on test runs (failing test source, regressions, green-test finish) did not fire. Now tether runs the test command itself, keeps the first N lines and adds `[... K lines cut by head; the end of the output:]` with the last 6 lines and the real `[exit N]`. In the v0.2.15 Polyglot run on qwen3.5:9b, 22 of 39 failing runs had at least one test result cut this way, against 7 of 47 passing runs. On 4 of those exercises plus 2 passing controls (32k, one run each), every test result now kept its summary, but the same 4 of 6 passed with and without the fix: it repairs what the model sees, not yet the pass rate (#173).
39
+
3
40
  ## v0.2.16 (2026-11-07)
4
41
 
5
42
  The default context is now 32k tokens, which on an 8 GB GPU keeps qwen3.5:9b fully on the GPU and passed 7 of 18 runs that hit the time cap at 8k, against 2. `tether run` shows that it is alive (and `--verbose` prints one line per step), an answer written as plain text is no longer lost, and a file written with a bare shell command is no longer reported as unchanged. No full Polyglot run was made for this release; the numbers below are from targeted labs.
@@ -64,7 +64,7 @@ A session whose last test run passed after its final change ends `COMPLETED` ins
64
64
 
65
65
  **Cause.** One very long generation can drop content, and `edit write` replaces the whole file (it reports what an overwrite removed).
66
66
 
67
- **Fix.** Write one small file per section and join them with a tool, rewrite the complete file, or use `edit replace` for a range. See [Built-in Tools](tutorial/built-in-tools.md#example-course-builder).
67
+ **Fix.** Write the file in parts with `edit append <path>` (each part in `stdin`), rewrite the complete file, or use `edit replace` for a range. See [Built-in Tools](tutorial/built-in-tools.md#example-course-builder).
68
68
 
69
69
  ### The model repeats a tool call with a value that was rejected
70
70
 
@@ -35,6 +35,8 @@ act(cmd="bash grep -n \"return list\" app/main.py | head")
35
35
  act(cmd="bash", stdin="python3 - << 'EOF'\nimport math\nprint(math.pi)\nEOF")
36
36
  ```
37
37
 
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
+
38
40
  ## `edit` — Workspace-Jailed File Operations
39
41
 
40
42
  Provides deterministic line-numbered viewing and `stdin`-based writing/replacement without sed/awk quoting bugs:
@@ -46,6 +48,9 @@ act(cmd="edit view tether/harness.py --lines 1:40")
46
48
  # Create or overwrite a file via stdin
47
49
  act(cmd="edit write notes/summary.md", stdin="# Summary\n\n- Point 1\n")
48
50
 
51
+ # A one-line file can take its text after the path (no stdin)
52
+ act(cmd="edit write VERSION 1.4.0")
53
+
49
54
  # Replace lines start:end (1-indexed, inclusive) with stdin content
50
55
  act(cmd="edit replace notes/summary.md --lines 3:3", stdin="- Updated Point 1")
51
56
  ```
@@ -84,7 +89,7 @@ act(cmd="mem recall obs_3")
84
89
 
85
90
  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:
86
91
 
87
- - **`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. The planner skill says so explicitly: "write the COMPLETE plan in ONE call". (An `edit append` is proposed in [#34](https://github.com/igormcsouza/tether/issues/34).)
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.
88
93
  - **`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.
89
94
  - **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.
90
95
 
@@ -72,7 +72,7 @@ Each stage needs a `goal`. Invalid manifests print `Error: invalid pipeline ...`
72
72
  tether stats [--model <m>] [--skill <s>] [--limit <n>]
73
73
  ```
74
74
 
75
- Prints per-model runs, pass rate, average steps, peak and total tokens, and friction counts (`nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`), followed by the commands that cause the most friction. Pipeline root sessions are excluded so tokens are not double-counted.
75
+ Prints per-model runs, pass rate, average steps, peak and total tokens, and friction counts (`nudge`, `loop_stall`, `cycle_stall`, `syntax_error`, `tool_error`, `gate_fail`, `gate_error`), followed by the commands that cause the most friction (keyed by tool and verb, such as `edit replace`; a leading `cd <dir> &&` is skipped, so `cd src && pytest` counts as `pytest`, and a turn with no command is `(no command)`). Pipeline root sessions are excluded so tokens are not double-counted.
76
76
 
77
77
  ## Post-mortem rules (`tether retro`)
78
78
 
@@ -84,3 +84,15 @@ def test_pipeline_runs_stages_in_order_with_an_upstream_digest(tether: Tether, c
84
84
  second = next(b for b in chaos.requests if "STAGE-TWO" in str(b["messages"][1]))
85
85
  assert "kiwi" in str(second["messages"]), "stage 2 must see a digest of stage 1"
86
86
  assert [t["function"]["name"] for t in second["tools"]] == ["act"]
87
+
88
+
89
+ def test_pipeline_row_shows_its_stages_model(tether: Tether, chaos) -> None:
90
+ (tether.ws / "one.yaml").write_text(
91
+ "name: one\nstages:\n - id: only\n"
92
+ " model: e2e/scripted\n goal: 'ROW-MODEL {{goal}}'\n"
93
+ )
94
+ chaos.route("ROW-MODEL", ("done", "ok"))
95
+ p = tether.cli("pipeline", "--json", "one.yaml", "note")
96
+ assert p.returncode == 0, p.stderr
97
+ root = next(s for s in _sessions(tether) if not s["parent_id"])
98
+ assert root["model"] == "e2e/scripted", root["model"]
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+
5
7
  from e2e.conftest import Tether
6
8
 
7
9
 
@@ -15,7 +17,19 @@ def test_verbose_prints_one_stderr_line_per_step_and_keeps_stdout(tether: Tether
15
17
  assert "\r" not in p.stderr # no dots when stderr is not a terminal
16
18
 
17
19
 
18
- def test_a_piped_run_without_verbose_is_silent_on_stderr(tether: Tether, chaos) -> None:
20
+ def test_a_piped_run_without_verbose_prints_only_the_session_id_on_stderr(
21
+ tether: Tether, chaos
22
+ ) -> None:
19
23
  chaos.script(("done", "ok"))
20
24
  p = tether.cli("run", "--model", "e2e/scripted", "go")
21
- assert (p.stdout, p.stderr) == ("ok\n", "")
25
+ assert p.stdout == "ok\n"
26
+ sid = p.stderr.removeprefix("session ").strip() # #90: there before the first model call
27
+ assert p.stderr == f"session {sid}\n" and sid.startswith("ses_")
28
+ assert tether.cli("status", sid).returncode == 0
29
+
30
+
31
+ def test_a_killed_json_run_still_names_its_session_on_stderr(tether: Tether, chaos) -> None:
32
+ chaos.script(("done", "ok"))
33
+ p = tether.cli("run", "--json", "--model", "e2e/scripted", "go")
34
+ assert p.stderr.startswith("session ses_")
35
+ assert p.stderr.split()[1] == json.loads(p.stdout)["id"] # stdout JSON unchanged
@@ -16,6 +16,13 @@ def test_heredoc_in_cmd_becomes_stdin(tether: Tether, chaos) -> None:
16
16
  assert (tether.ws / "a.txt").read_text() == "hello"
17
17
 
18
18
 
19
+ def test_shell_file_write_runs_as_edit_without_bash(tether: Tether, chaos) -> None:
20
+ chaos.script("echo ok > hello.txt", "cat hello.txt", ("done", "ok"))
21
+ r = tether.run("Create hello.txt containing ok", "--tools", "edit")
22
+ assert (tether.ws / "hello.txt").read_text() == "ok\n"
23
+ assert tether.events(r["id"])[:2] == ["ok", "ok"]
24
+
25
+
19
26
  def test_bare_shell_command_runs_through_bash(tether: Tether, chaos) -> None:
20
27
  (tether.ws / "marker.txt").write_text("x")
21
28
  chaos.script("ls", ("done", "ok"))
@@ -60,6 +67,22 @@ def test_literal_backslash_n_in_one_line_stdin_is_unescaped(tether: Tether, chao
60
67
  assert (tether.ws / "a.txt").read_text() == "one\ntwo"
61
68
 
62
69
 
70
+ def test_edit_write_with_inline_content_writes_it(tether: Tether, chaos) -> None:
71
+ chaos.script("edit write hello.txt ok", ("done", "ok")) # qwen3.5:9b in the v0.2.16 smoke run
72
+ r = tether.run("inline", "--tools", "edit")
73
+ assert tether.events(r["id"])[0] == "ok" and (tether.ws / "hello.txt").read_text() == "ok\n"
74
+
75
+
76
+ def test_test_run_piped_to_head_keeps_its_summary(tether: Tether, chaos) -> None:
77
+ body = "".join(f" def test_{i}(self):\n assert {i} == -1\n" for i in range(20))
78
+ suite = f"import unittest\n\n\nclass T(unittest.TestCase):\n{body}"
79
+ (tether.ws / "x_test.py").write_text(suite)
80
+ chaos.script("python3 -m unittest discover -p '*_test.py' 2>&1 | head -20", ("done", "ok"))
81
+ r = tether.run("tests", "--tools", "bash")
82
+ out = tether.tool_outputs(r["id"])[0]
83
+ assert "cut by `head`" in out and "FAILED (failures=20)" in out and "[exit 1]" in out
84
+
85
+
63
86
  def test_plain_text_reply_gets_a_nudge_not_a_completion(tether: Tether, chaos) -> None:
64
87
  chaos.script({"text": "I will think about it."}, ("done", "ok"))
65
88
  r = tether.run("nudge")
@@ -256,3 +279,21 @@ def test_act_call_typed_as_text_is_unwrapped(tether: Tether, chaos) -> None:
256
279
  chaos.script('act("done", stdin="all green")')
257
280
  r = tether.run("finish", "--tools", "bash")
258
281
  assert r["status"] == "COMPLETED" and r["result"] == "all green"
282
+
283
+
284
+ def test_edit_append_builds_a_file_in_parts(tether: Tether, chaos) -> None:
285
+ chaos.script(
286
+ ("edit append plan.md", "# Module 1\n"),
287
+ ("edit append plan.md", "# Module 2\n"),
288
+ ("done", "ok"),
289
+ )
290
+ tether.run("plan", "--tools", "edit")
291
+ assert (tether.ws / "plan.md").read_text() == "# Module 1\n# Module 2\n"
292
+
293
+
294
+ def test_a_cd_into_a_missing_container_dir_is_skipped(tether: Tether, chaos) -> None:
295
+ (tether.ws / "marker.txt").write_text("x")
296
+ chaos.script("cd /testbed && ls", ("done", "ok"))
297
+ r = tether.run("list", "--tools", "bash")
298
+ out = tether.tool_outputs(r["id"])[0]
299
+ assert "`cd /testbed` skipped" in out and "marker.txt" in out
@@ -0,0 +1,65 @@
1
+ """Restart after repeated failing check runs: what tether sends and records (scripted, no model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+
9
+ from e2e.conftest import Tether
10
+
11
+ TEST = (
12
+ "import unittest\nimport a\n\n\nclass T(unittest.TestCase):\n"
13
+ " def test_x(self):\n self.assertEqual(a.X, 2)\n"
14
+ )
15
+ RUN = "bash -c 'python3 -m unittest test_t 2>&1'"
16
+
17
+
18
+ def test_third_failing_check_restarts_with_a_description_not_a_replay(
19
+ tether: Tether, chaos
20
+ ) -> None:
21
+ (tether.ws / "test_t.py").write_text(TEST)
22
+ chaos.script( # after the first run, tether re-runs the tests after each edit ([auto-test])
23
+ ("edit write a.py", "X = 1 # MARKER_FIRST\n"), RUN,
24
+ ("edit write a.py", "X = 3\n"),
25
+ ("edit write a.py", "X = 4 # LATEST\n"),
26
+ ("edit write a.py", "X = 5\n"),
27
+ ("done", "ok"),
28
+ ) # fmt: skip
29
+ d = tether.run("make the test pass", "--tools", "edit,bash", "--max-steps", "20")
30
+ turns = tether.turns(d["id"])
31
+ restarts = [t for t in turns if t["event_type"] == "restart"]
32
+ assert len(restarts) == 1, "one restart per session"
33
+ assert (
34
+ "X = 4 # LATEST" in restarts[0]["content"] and "MARKER_FIRST" not in restarts[0]["content"]
35
+ )
36
+ after = json.dumps(chaos.requests[4]["messages"])
37
+ assert "[RESTART at step 4" in after and "MARKER_FIRST" not in after
38
+ assert "[auto-test]" in json.dumps(chaos.requests[3]["messages"]), "edits ran the tests"
39
+ assert "MARKER_FIRST" in json.dumps(chaos.requests[3]["messages"]), "before it, full history"
40
+ assert "MARKER_FIRST" in "".join(t["tool_stdin"] or "" for t in turns), "store keeps it all"
41
+
42
+
43
+ def test_a_near_pass_comes_back_in_place_and_the_history_stays(tether: Tether, chaos) -> None:
44
+ tests = "".join(
45
+ f" def test_{n}(self):\n self.assertEqual(a.{n.upper()}, 1)\n" for n in "uvwx"
46
+ )
47
+ (tether.ws / "test_t.py").write_text(
48
+ f"import unittest\nimport a\n\n\nclass T(unittest.TestCase):\n{tests}"
49
+ )
50
+ stub = tether.ws / "a.py" # a starting file: only those are put back
51
+ stub.write_text("U = 0\n")
52
+ os.utime(stub, (time.time() - 100,) * 2)
53
+ best = "U = V = W = 1 # BEST\n"
54
+ chaos.script(
55
+ ("edit write a.py", best), RUN,
56
+ ("edit write a.py", "U = V = 1\n"),
57
+ ("edit write a.py", "U = 1 # WORST\n"),
58
+ ("done", "ok"),
59
+ ) # fmt: skip
60
+ d = tether.run("make the tests pass", "--tools", "edit,bash", "--max-steps", "20")
61
+ turns = tether.turns(d["id"])
62
+ assert not [t for t in turns if t["event_type"] == "restart"]
63
+ assert any("[RESTORED]" in t["content"] and "test_x" in t["content"] for t in turns)
64
+ assert (tether.ws / "a.py").read_text() == best
65
+ assert "# BEST" in json.dumps(chaos.requests[4]["messages"]), "history kept"
@@ -24,7 +24,7 @@
24
24
  "role": "assistant"
25
25
  },
26
26
  {
27
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
27
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
28
28
  "role": "user"
29
29
  }
30
30
  ],
@@ -42,7 +42,7 @@
42
42
  "role": "assistant"
43
43
  },
44
44
  {
45
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
45
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
46
46
  "role": "user"
47
47
  },
48
48
  {
@@ -68,7 +68,7 @@
68
68
  "role": "assistant"
69
69
  },
70
70
  {
71
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
71
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
72
72
  "role": "user"
73
73
  },
74
74
  {
@@ -102,7 +102,7 @@
102
102
  "role": "assistant"
103
103
  },
104
104
  {
105
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
105
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
106
106
  "role": "user"
107
107
  },
108
108
  {
@@ -144,7 +144,7 @@
144
144
  "role": "assistant"
145
145
  },
146
146
  {
147
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
147
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
148
148
  "role": "user"
149
149
  },
150
150
  {
@@ -194,7 +194,7 @@
194
194
  "role": "assistant"
195
195
  },
196
196
  {
197
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
197
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
198
198
  "role": "user"
199
199
  },
200
200
  {
@@ -252,7 +252,7 @@
252
252
  "role": "assistant"
253
253
  },
254
254
  {
255
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
255
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
256
256
  "role": "user"
257
257
  },
258
258
  {
@@ -318,7 +318,7 @@
318
318
  "role": "assistant"
319
319
  },
320
320
  {
321
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
321
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
322
322
  "role": "user"
323
323
  },
324
324
  {
@@ -392,7 +392,7 @@
392
392
  "role": "assistant"
393
393
  },
394
394
  {
395
- "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill).",
395
+ "content": "Observation:\n[obs_1] Error: unknown command `sed`. Enabled tools: done, edit. Enable more: --tools bash,edit (CLI), tools=[...] (SDK), `tools:` (skill). Files: `edit view <path>`, `edit write <path>` with the text in stdin.",
396
396
  "role": "user"
397
397
  },
398
398
  {
@@ -476,7 +476,7 @@
476
476
  "files": {
477
477
  "calc.py": "def total(xs):\n t = 0\n for x in xs:\n t += x\n return t\n\n\ndef mean(xs):\n return total(xs) / len(xs) # fixed\n"
478
478
  },
479
- "peak_tokens": 655,
479
+ "peak_tokens": 673,
480
480
  "result": "Fixed off-by-one in total().",
481
481
  "status": "COMPLETED",
482
482
  "step_count": 10