tsugite-cli 0.14.3__tar.gz → 0.15.0__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 (225) hide show
  1. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/AGENTS.md +3 -0
  2. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/PKG-INFO +1 -1
  3. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/pyproject.toml +3 -1
  4. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_inheritance.py +140 -38
  5. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/runner.py +36 -8
  6. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_utils.py +36 -22
  7. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_agents/default.md +55 -0
  8. tsugite_cli-0.15.0/tsugite/builtin_agents/job_verifier.md +58 -0
  9. tsugite_cli-0.15.0/tsugite/builtin_agents/job_worker.md +93 -0
  10. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/agent.py +73 -5
  11. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/claude_code.py +38 -9
  12. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/executor.py +21 -2
  13. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/adapters/base.py +92 -12
  14. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/adapters/http.py +453 -28
  15. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/adapters/scheduler_adapter.py +9 -0
  16. tsugite_cli-0.15.0/tsugite/daemon/commands.py +435 -0
  17. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/compaction_scheduler.py +15 -0
  18. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/gateway.py +82 -2
  19. tsugite_cli-0.15.0/tsugite/daemon/job_store.py +242 -0
  20. tsugite_cli-0.15.0/tsugite/daemon/jobs_orchestrator.py +1410 -0
  21. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/memory.py +36 -0
  22. tsugite_cli-0.15.0/tsugite/daemon/pty_manager.py +401 -0
  23. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/push.py +4 -1
  24. tsugite_cli-0.15.0/tsugite/daemon/record_store.py +146 -0
  25. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/scheduler.py +125 -55
  26. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/session_runner.py +39 -17
  27. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/session_store.py +54 -7
  28. tsugite_cli-0.15.0/tsugite/daemon/terminal_runtime.py +176 -0
  29. tsugite_cli-0.15.0/tsugite/daemon/terminal_store.py +132 -0
  30. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/css/console.css +18 -62
  31. tsugite_cli-0.15.0/tsugite/daemon/web/css/job-flows.css +91 -0
  32. tsugite_cli-0.15.0/tsugite/daemon/web/css/jobs-tab.css +158 -0
  33. tsugite_cli-0.15.0/tsugite/daemon/web/css/jobs.css +203 -0
  34. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/css/responsive.css +45 -11
  35. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/css/styles.css +6 -68
  36. tsugite_cli-0.15.0/tsugite/daemon/web/css/terminal.css +335 -0
  37. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/css/theme.css +4 -0
  38. tsugite_cli-0.15.0/tsugite/daemon/web/css/tsu-modal.css +298 -0
  39. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/index.html +961 -107
  40. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/app.js +51 -2
  41. tsugite_cli-0.15.0/tsugite/daemon/web/js/utils/tsu-modal.js +66 -0
  42. tsugite_cli-0.15.0/tsugite/daemon/web/js/utils/xterm-loader.js +183 -0
  43. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/utils.js +28 -0
  44. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversation/attachments.js +2 -3
  45. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversation/history.js +102 -1
  46. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversation/input.js +79 -11
  47. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversation/sessions.js +26 -10
  48. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversations.js +235 -8
  49. tsugite_cli-0.15.0/tsugite/daemon/web/js/views/jobs.js +367 -0
  50. tsugite_cli-0.15.0/tsugite/daemon/web/js/views/terminals.js +734 -0
  51. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/md_agents.py +1 -0
  52. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/models.py +22 -11
  53. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/anthropic.py +1 -0
  54. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/claude_code.py +14 -1
  55. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/schemas/agent.schema.json +5 -0
  56. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/__init__.py +19 -1
  57. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/agents.py +14 -18
  58. tsugite_cli-0.15.0/tsugite/tools/jobs.py +190 -0
  59. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/schedule.py +9 -14
  60. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/sessions.py +2 -11
  61. tsugite_cli-0.15.0/tsugite/tools/terminal.py +319 -0
  62. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/repl_completer.py +5 -20
  63. tsugite_cli-0.14.3/tsugite/daemon/commands.py +0 -206
  64. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/.gitignore +0 -0
  65. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/CONTRIBUTING.md +0 -0
  66. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/LICENSE +0 -0
  67. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/README.md +0 -0
  68. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/__init__.py +0 -0
  69. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_preparation.py +0 -0
  70. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/__init__.py +0 -0
  71. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/exec_directives.py +0 -0
  72. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/exec_runner.py +0 -0
  73. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/helpers.py +0 -0
  74. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/history_integration.py +0 -0
  75. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/metrics.py +0 -0
  76. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/models.py +0 -0
  77. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/agent_runner/validation.py +0 -0
  78. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/__init__.py +0 -0
  79. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/auto_context.py +0 -0
  80. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/base.py +0 -0
  81. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/file.py +0 -0
  82. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/inline.py +0 -0
  83. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/storage.py +0 -0
  84. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/url.py +0 -0
  85. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/attachments/youtube.py +0 -0
  86. {tsugite_cli-0.14.3/tsugite/builtin_skills → tsugite_cli-0.15.0/tsugite/builtin_agents}/.gitkeep +0 -0
  87. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_agents/code_searcher.md +0 -0
  88. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_agents/file_searcher.md +0 -0
  89. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_agents/onboard.md +0 -0
  90. {tsugite_cli-0.14.3/tsugite/builtin_agents → tsugite_cli-0.15.0/tsugite/builtin_skills}/.gitkeep +0 -0
  91. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  92. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  93. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  94. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  95. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  96. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  97. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  98. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  99. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cache.py +0 -0
  100. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/__init__.py +0 -0
  101. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/agents.py +0 -0
  102. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/attachments.py +0 -0
  103. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/cache.py +0 -0
  104. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/chat.py +0 -0
  105. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/config.py +0 -0
  106. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/daemon.py +0 -0
  107. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/helpers.py +0 -0
  108. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/history.py +0 -0
  109. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/init.py +0 -0
  110. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/models.py +0 -0
  111. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/plugins.py +0 -0
  112. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/render.py +0 -0
  113. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/run.py +0 -0
  114. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/secrets.py +0 -0
  115. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/skills.py +0 -0
  116. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/tools.py +0 -0
  117. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/usage.py +0 -0
  118. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/validate.py +0 -0
  119. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/cli/workspace.py +0 -0
  120. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/config.py +0 -0
  121. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/console.py +0 -0
  122. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/constants.py +0 -0
  123. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/__init__.py +0 -0
  124. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/content_blocks.py +0 -0
  125. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/memory.py +0 -0
  126. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/proxy.py +0 -0
  127. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/sandbox.py +0 -0
  128. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/state.py +0 -0
  129. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/subprocess_executor.py +0 -0
  130. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/core/tools.py +0 -0
  131. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/__init__.py +0 -0
  132. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/adapters/__init__.py +0 -0
  133. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/adapters/discord.py +0 -0
  134. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/auth.py +0 -0
  135. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/config.py +0 -0
  136. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/icons/icon-192.png +0 -0
  137. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
  138. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/icons/icon-512.png +0 -0
  139. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
  140. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
  141. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/api.js +0 -0
  142. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/vendor/marked.LICENSE.md +0 -0
  143. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/vendor/marked.esm.min.js +0 -0
  144. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversation/event_types.js +0 -0
  145. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/conversation/streaming.js +0 -0
  146. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/file-editor.js +0 -0
  147. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/schedules.js +0 -0
  148. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/usage.js +0 -0
  149. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/webhooks.js +0 -0
  150. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/js/views/workspace.js +0 -0
  151. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/manifest.json +0 -0
  152. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/web/sw.js +0 -0
  153. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/daemon/webhook_store.py +0 -0
  154. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/events/__init__.py +0 -0
  155. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/events/base.py +0 -0
  156. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/events/bus.py +0 -0
  157. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/events/events.py +0 -0
  158. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/events/helpers.py +0 -0
  159. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/exceptions.py +0 -0
  160. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/history/__init__.py +0 -0
  161. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/history/models.py +0 -0
  162. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/history/reconstruction.py +0 -0
  163. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/history/storage.py +0 -0
  164. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/hooks.py +0 -0
  165. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/interaction.py +0 -0
  166. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/options.py +0 -0
  167. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/plugins.py +0 -0
  168. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/__init__.py +0 -0
  169. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/base.py +0 -0
  170. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/model_cache.py +0 -0
  171. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/model_registry.py +0 -0
  172. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/ollama.py +0 -0
  173. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/openai_compat.py +0 -0
  174. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/providers/openrouter.py +0 -0
  175. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/renderer.py +0 -0
  176. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/schemas/__init__.py +0 -0
  177. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/__init__.py +0 -0
  178. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/backend.py +0 -0
  179. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/env.py +0 -0
  180. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/exec.py +0 -0
  181. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/file.py +0 -0
  182. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/masking.py +0 -0
  183. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/registry.py +0 -0
  184. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/secrets/sqlite.py +0 -0
  185. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/shell_tool_config.py +0 -0
  186. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/skill_discovery.py +0 -0
  187. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/AGENTS.md +0 -0
  188. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/IDENTITY.md +0 -0
  189. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/MEMORY.md +0 -0
  190. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/USER.md +0 -0
  191. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/personas/casual-technical.md +0 -0
  192. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/personas/marvin.md +0 -0
  193. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/templates/personas/minimal.md +0 -0
  194. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/fs.py +0 -0
  195. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/history.py +0 -0
  196. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/http.py +0 -0
  197. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/interactive.py +0 -0
  198. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/notify.py +0 -0
  199. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/scratchpad.py +0 -0
  200. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/secrets.py +0 -0
  201. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/shell.py +0 -0
  202. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/shell_tools.py +0 -0
  203. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/skills.py +0 -0
  204. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tools/time.py +0 -0
  205. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/tsugite.py +0 -0
  206. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/__init__.py +0 -0
  207. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/base.py +0 -0
  208. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/chat.py +0 -0
  209. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/helpers.py +0 -0
  210. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/jsonl.py +0 -0
  211. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/live.py +0 -0
  212. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/plain.py +0 -0
  213. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/repl_chat.py +0 -0
  214. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/repl_commands.py +0 -0
  215. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui/repl_handler.py +0 -0
  216. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/ui_context.py +0 -0
  217. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/usage/__init__.py +0 -0
  218. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/usage/store.py +0 -0
  219. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/user_agent.py +0 -0
  220. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/utils.py +0 -0
  221. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/workspace/__init__.py +0 -0
  222. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/workspace/context.py +0 -0
  223. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/workspace/models.py +0 -0
  224. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/workspace/session.py +0 -0
  225. {tsugite_cli-0.14.3 → tsugite_cli-0.15.0}/tsugite/workspace/templates.py +0 -0
@@ -24,6 +24,9 @@ uv run pytest --cov=tsugite --cov-report=html
24
24
  uv run black .
25
25
  uv run ruff check .
26
26
  uv run pylint tsugite
27
+
28
+ # One-time: enable repo pre-commit hook (lint + format on commit)
29
+ git config core.hooksPath .githooks
27
30
  ```
28
31
 
29
32
  ### Testing Agents
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.14.3
3
+ Version: 0.15.0
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.14.3"
3
+ version = "0.15.0"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -67,6 +67,7 @@ members = ["plugins/*"]
67
67
  tsugite-cli = { workspace = true }
68
68
  tsugite-tmux = { workspace = true }
69
69
  tsugite-acp = { workspace = true }
70
+ tsugite-codex-cli = { workspace = true }
70
71
 
71
72
  [tool.ruff]
72
73
  target-version = "py311"
@@ -142,4 +143,5 @@ dev = [
142
143
  "vulture>=2.14",
143
144
  "tsugite-tmux",
144
145
  "tsugite-acp",
146
+ "tsugite-codex-cli",
145
147
  ]
@@ -1,11 +1,38 @@
1
1
  """Agent inheritance resolution for Tsugite."""
2
2
 
3
+ import importlib.metadata
4
+ import importlib.util
5
+ import logging
3
6
  import os
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from functools import lru_cache
4
10
  from pathlib import Path
5
11
  from typing import Any, Dict, List, Optional, Set
6
12
 
7
13
  from tsugite.utils import ensure_file_exists
8
14
 
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class AgentDirSource(Enum):
19
+ """Origin of an agent search directory."""
20
+
21
+ WORKSPACE = "workspace" # caller-passed workspace agents/ (mutable)
22
+ PROJECT = "project" # caller-passed project agents/ (mutable; today only http.py)
23
+ BUILTIN = "builtin" # tsugite/builtin_agents/ (read-only)
24
+ PLUGIN = "plugin" # plugin <pkg>/agents/ via tsugite.plugins entry-points (read-only)
25
+ GLOBAL = "global" # XDG global agents/ (mutable)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class AgentDir:
30
+ """A directory that may contain agent .md files."""
31
+
32
+ path: Path
33
+ source: AgentDirSource
34
+ readonly: bool
35
+
9
36
 
10
37
  def get_builtin_agents_path() -> Path:
11
38
  """Get the built-in agents directory path.
@@ -37,6 +64,113 @@ def get_global_agents_paths() -> List[Path]:
37
64
  return paths
38
65
 
39
66
 
67
+ @lru_cache(maxsize=1)
68
+ def get_plugin_agents_paths() -> List[Path]:
69
+ """Get agent directory paths contributed by installed plugins.
70
+
71
+ Scans the ``tsugite.plugins`` entry-point group; each plugin's package
72
+ may ship an ``agents/`` directory next to its top-level module. Plugin
73
+ agent dirs are treated as read-only.
74
+
75
+ Returns:
76
+ List of plugin-supplied agent directories (deduped, only existing dirs)
77
+ """
78
+ from tsugite.plugins import GROUP_PLUGINS
79
+
80
+ paths: List[Path] = []
81
+ seen: set[Path] = set()
82
+ try:
83
+ eps = importlib.metadata.entry_points(group=GROUP_PLUGINS)
84
+ except Exception as e: # pragma: no cover - defensive
85
+ logger.debug("Failed to scan plugin entry points: %s", e)
86
+ return paths
87
+
88
+ for ep in eps:
89
+ module_name = ep.value.split(":")[0].strip()
90
+ top_level = module_name.split(".")[0]
91
+ try:
92
+ spec = importlib.util.find_spec(top_level)
93
+ except Exception:
94
+ spec = None
95
+ if spec is None or not spec.origin:
96
+ continue
97
+ agents_dir = Path(spec.origin).parent / "agents"
98
+ resolved = agents_dir.resolve()
99
+ if resolved in seen:
100
+ continue
101
+ if not agents_dir.is_dir():
102
+ continue
103
+ seen.add(resolved)
104
+ paths.append(agents_dir)
105
+ return paths
106
+
107
+
108
+ def iter_agent_search_paths(
109
+ *,
110
+ current_agent_dir: Optional[Path] = None,
111
+ workspace: Optional[Any] = None,
112
+ extra_project_dirs: Optional[List[Path]] = None,
113
+ include_local_roots: bool = True,
114
+ ) -> List[AgentDir]:
115
+ """Yield the canonical agent-search order, deduped by resolved path.
116
+
117
+ Order:
118
+
119
+ 1. workspace.agents_dir (if workspace given)
120
+ 2. current_agent_dir / ".tsugite"
121
+ 3. current_agent_dir / ".tsugite" / "agents"
122
+ 4. current_agent_dir / "agents"
123
+ 5. current_agent_dir itself (for `<name>.md` directly in cwd)
124
+ 6. caller-supplied extra project dirs (HTTP adapter feeds per-agent dirs here)
125
+ 7. builtin agents
126
+ 8. XDG global agents (user agents beat plugin agents of the same name)
127
+ 9. plugin-supplied agents
128
+
129
+ include_local_roots: when False, skip entries 2 and 5 (the bare .tsugite/
130
+ and cwd roots). Name-resolution callers want them so `extends: foo` finds a
131
+ sibling foo.md; discovery callers that glob *.md must skip them, otherwise
132
+ every frontmattered note in a docs/notes workspace lists as an agent.
133
+
134
+ Callers that only care about a subset filter on ``AgentDir.source``.
135
+ """
136
+ results: List[AgentDir] = []
137
+ seen: set[Path] = set()
138
+
139
+ def _add(path: Path, source: AgentDirSource, readonly: bool) -> None:
140
+ try:
141
+ resolved = path.resolve()
142
+ except (OSError, RuntimeError):
143
+ return
144
+ if resolved in seen:
145
+ return
146
+ seen.add(resolved)
147
+ results.append(AgentDir(path=path, source=source, readonly=readonly))
148
+
149
+ if workspace is not None and hasattr(workspace, "agents_dir"):
150
+ _add(workspace.agents_dir, AgentDirSource.WORKSPACE, readonly=False)
151
+
152
+ if current_agent_dir is not None:
153
+ if include_local_roots:
154
+ _add(current_agent_dir / ".tsugite", AgentDirSource.PROJECT, readonly=False)
155
+ _add(current_agent_dir / ".tsugite" / "agents", AgentDirSource.PROJECT, readonly=False)
156
+ _add(current_agent_dir / "agents", AgentDirSource.PROJECT, readonly=False)
157
+ if include_local_roots:
158
+ _add(current_agent_dir, AgentDirSource.PROJECT, readonly=False)
159
+
160
+ for extra in extra_project_dirs or []:
161
+ _add(extra, AgentDirSource.PROJECT, readonly=False)
162
+
163
+ _add(get_builtin_agents_path(), AgentDirSource.BUILTIN, readonly=True)
164
+
165
+ for global_dir in get_global_agents_paths():
166
+ _add(global_dir, AgentDirSource.GLOBAL, readonly=False)
167
+
168
+ for plugin_path in get_plugin_agents_paths():
169
+ _add(plugin_path, AgentDirSource.PLUGIN, readonly=True)
170
+
171
+ return results
172
+
173
+
40
174
  def find_agent_file(
41
175
  agent_ref: str,
42
176
  current_agent_dir: Path,
@@ -52,14 +186,8 @@ def find_agent_file(
52
186
  Returns:
53
187
  Path to agent file if found, None otherwise
54
188
 
55
- Search order:
56
- 1. If path-like (contains / or .md), resolve relative to current agent
57
- 2. Workspace agents directory (if workspace provided)
58
- 3. .tsugite/{name}.md (project-local shared)
59
- 4. ./agents/{name}.md (project convention)
60
- 5. ./{name}.md (current directory)
61
- 6. Built-in agents directory (tsugite/builtin_agents/)
62
- 7. Global agent directories (XDG order)
189
+ Search order matches `iter_agent_search_paths`: workspace agents,
190
+ project-local (.tsugite/, agents/, current dir), builtin, global, plugin.
63
191
  """
64
192
  # If it looks like a path, resolve it relative to current agent
65
193
  if "/" in agent_ref or agent_ref.endswith(".md"):
@@ -71,38 +199,12 @@ def find_agent_file(
71
199
  return abs_path.resolve()
72
200
  return None
73
201
 
74
- # Ensure .md extension for name-based lookup
75
202
  agent_name = agent_ref if agent_ref.endswith(".md") else f"{agent_ref}.md"
76
203
 
77
- search_paths = []
78
-
79
- # Workspace agents directory (highest priority)
80
- if workspace and hasattr(workspace, "agents_dir"):
81
- workspace_agents = workspace.agents_dir
82
- if workspace_agents.exists():
83
- search_paths.append(workspace_agents / agent_name)
84
-
85
- # Project-local locations
86
- search_paths.extend(
87
- [
88
- current_agent_dir / ".tsugite" / agent_name,
89
- current_agent_dir / "agents" / agent_name,
90
- current_agent_dir / agent_name,
91
- ]
92
- )
93
-
94
- # Built-in agents directory
95
- builtin_path = get_builtin_agents_path() / agent_name
96
- search_paths.append(builtin_path)
97
-
98
- # Global locations
99
- for global_dir in get_global_agents_paths():
100
- search_paths.append(global_dir / agent_name)
101
-
102
- # Return first existing path
103
- for path in search_paths:
104
- if path.exists():
105
- return path.resolve()
204
+ for entry in iter_agent_search_paths(current_agent_dir=current_agent_dir, workspace=workspace):
205
+ candidate = entry.path / agent_name
206
+ if candidate.exists():
207
+ return candidate.resolve()
106
208
 
107
209
  return None
108
210
 
@@ -14,7 +14,7 @@ from tsugite.core.agent import TsugiteAgent # noqa: E402
14
14
  from tsugite.core.executor import LocalExecutor # noqa: E402
15
15
  from tsugite.exceptions import AgentExecutionError, is_prompt_too_long_error # noqa: E402
16
16
  from tsugite.md_agents import AgentConfig, parse_agent_file # noqa: E402
17
- from tsugite.models import resolve_effective_model # noqa: E402
17
+ from tsugite.models import resolve_effective_model, strip_reserved_model_kwargs # noqa: E402
18
18
  from tsugite.options import ExecutionOptions # noqa: E402
19
19
  from tsugite.renderer import AgentRenderer # noqa: E402
20
20
  from tsugite.utils import is_interactive # noqa: E402
@@ -411,6 +411,7 @@ async def _execute_agent_with_prompt(
411
411
  hook_vars: Optional[Dict[str, str]] = None,
412
412
  continue_conversation_id: Optional[str] = None,
413
413
  user_input_for_history: Optional[str] = None,
414
+ channel_metadata: Optional[Dict[str, Any]] = None,
414
415
  ) -> str | AgentExecutionResult:
415
416
  """Execute agent with a prepared agent.
416
417
 
@@ -491,13 +492,29 @@ async def _execute_agent_with_prompt(
491
492
  # Get model string
492
493
  model_string = _get_model_string(exec_options.model_override, agent_config)
493
494
 
494
- # Merge reasoning_effort. Resolution order: explicit kwargs > override > agent config.
495
- final_model_kwargs = dict(model_kwargs or {})
495
+ # Merge model_kwargs from the agent frontmatter first (lowest precedence), then
496
+ # explicit caller kwargs override. This lets agents declare e.g.
497
+ # `model_kwargs: {response_format: {type: json_object}}` once and have every
498
+ # invocation get structured output without each caller threading it through.
499
+ final_model_kwargs = {}
500
+ if hasattr(agent_config, "model_kwargs") and agent_config.model_kwargs:
501
+ final_model_kwargs.update(agent_config.model_kwargs)
502
+ final_model_kwargs.update(model_kwargs or {})
503
+ # Reject provider-call args (messages/model/stream) that would collide with the
504
+ # explicit keywords splatted alongside **model_kwargs in core/agent.py.
505
+ final_model_kwargs = strip_reserved_model_kwargs(final_model_kwargs)
506
+
507
+ # Resolve reasoning_effort. Precedence (highest wins): exec_options override >
508
+ # caller-supplied model_kwargs > agent.model_kwargs > agent.reasoning_effort field.
509
+ # The override is a user-facing CLI/daemon knob - it MUST beat any agent default,
510
+ # including one baked into agent.model_kwargs (which would otherwise shadow it).
511
+ # Caller kwargs and agent.model_kwargs are already in final_model_kwargs from the merge
512
+ # above, so only the override and the agent.reasoning_effort fallback resolve here.
496
513
  effort_override = getattr(exec_options, "reasoning_effort_override", None)
497
- if "reasoning_effort" not in final_model_kwargs:
498
- if effort_override:
499
- final_model_kwargs["reasoning_effort"] = effort_override
500
- elif hasattr(agent_config, "reasoning_effort") and agent_config.reasoning_effort:
514
+ if effort_override:
515
+ final_model_kwargs["reasoning_effort"] = effort_override
516
+ elif "reasoning_effort" not in final_model_kwargs:
517
+ if hasattr(agent_config, "reasoning_effort") and agent_config.reasoning_effort:
501
518
  final_model_kwargs["reasoning_effort"] = agent_config.reasoning_effort
502
519
 
503
520
  from tsugite.models import resolve_reasoning_effort
@@ -573,7 +590,12 @@ async def _execute_agent_with_prompt(
573
590
  # model_request event; replaying the whole thing here would clog the
574
591
  # chat bubble with noise the user never wrote.
575
592
  display_prompt = user_input_for_history or prepared.original_prompt or prepared.rendered_prompt
576
- record_user_input(session_storage, display_prompt, attachments=prepared.attachments)
593
+ record_user_input(
594
+ session_storage,
595
+ display_prompt,
596
+ attachments=prepared.attachments,
597
+ channel_metadata=channel_metadata,
598
+ )
577
599
  except Exception as e:
578
600
  logger.debug("Could not open session storage for live event recording: %s", e)
579
601
  session_storage = None
@@ -743,6 +765,7 @@ def run_agent(
743
765
  attachments: Optional[List[Any]] = None,
744
766
  path_context: Optional[Any] = None,
745
767
  user_input_for_history: Optional[str] = None,
768
+ channel_metadata: Optional[Dict[str, Any]] = None,
746
769
  ) -> str | AgentExecutionResult:
747
770
  """Run a Tsugite agent (sync wrapper around run_agent_async).
748
771
 
@@ -793,6 +816,7 @@ def run_agent(
793
816
  attachments=attachments,
794
817
  path_context=path_context,
795
818
  user_input_for_history=user_input_for_history,
819
+ channel_metadata=channel_metadata,
796
820
  )
797
821
  )
798
822
 
@@ -976,6 +1000,7 @@ async def run_agent_async(
976
1000
  hook_vars=hook_vars,
977
1001
  continue_conversation_id=continue_conversation_id,
978
1002
  user_input_for_history=user_input_for_history,
1003
+ channel_metadata=channel_metadata,
979
1004
  )
980
1005
  except (RuntimeError, AgentExecutionError) as e:
981
1006
  err_str = str(e).lower()
@@ -1001,6 +1026,7 @@ async def run_agent_async(
1001
1026
  hook_vars=hook_vars,
1002
1027
  continue_conversation_id=continue_conversation_id,
1003
1028
  user_input_for_history=user_input_for_history,
1029
+ channel_metadata=channel_metadata,
1004
1030
  )
1005
1031
  raise
1006
1032
  finally:
@@ -1026,6 +1052,8 @@ _MULTISTEP_FRAMEWORK_FLAG_DEFAULTS: Dict[str, Any] = {
1026
1052
  "has_notify_tool": False,
1027
1053
  "agent_name": "",
1028
1054
  "can_spawn_sessions": False,
1055
+ "can_spawn_jobs": False,
1056
+ "can_use_pty": False,
1029
1057
  "is_channel_session": False,
1030
1058
  "active_sessions": [],
1031
1059
  "recent_completions": [],
@@ -134,34 +134,48 @@ def list_local_agents(base_path: Path = None) -> dict[str, List[Path]]:
134
134
  base_path: Base directory to search from (defaults to cwd)
135
135
 
136
136
  Returns:
137
- Dictionary mapping location names to list of agent paths
137
+ Dictionary mapping location names to list of agent paths,
138
+ ordered: Built-in, Plugins, Current directory, .tsugite/, agents/.
138
139
  """
139
- from .agent_inheritance import get_builtin_agents_path
140
+ from .agent_inheritance import AgentDirSource, iter_agent_search_paths
140
141
 
141
142
  if base_path is None:
142
143
  base_path = Path.cwd()
143
144
 
144
- results = {}
145
-
146
- # Add built-in agents first
147
- builtin_path = get_builtin_agents_path()
148
- if builtin_path.exists() and builtin_path.is_dir():
149
- builtin_agents = sorted(builtin_path.glob("*.md"))
150
- if builtin_agents:
151
- results["Built-in"] = builtin_agents
152
-
153
- locations = [
154
- ("Current directory", base_path),
155
- (".tsugite/", base_path / ".tsugite"),
156
- ("agents/", base_path / "agents"),
157
- ]
145
+ project_labels = {
146
+ (base_path / ".tsugite").resolve(): ".tsugite/",
147
+ (base_path / ".tsugite" / "agents").resolve(): ".tsugite/agents/",
148
+ (base_path / "agents").resolve(): "agents/",
149
+ base_path.resolve(): "Current directory",
150
+ }
151
+ source_labels = {
152
+ AgentDirSource.BUILTIN: "Built-in",
153
+ AgentDirSource.PLUGIN: "Plugins",
154
+ }
155
+ grouped: dict[str, List[Path]] = {}
156
+
157
+ for entry in iter_agent_search_paths(current_agent_dir=base_path):
158
+ if entry.source == AgentDirSource.GLOBAL:
159
+ continue
160
+ if not entry.path.exists() or not entry.path.is_dir():
161
+ continue
162
+
163
+ if entry.source == AgentDirSource.PROJECT:
164
+ label = project_labels.get(entry.path.resolve())
165
+ else:
166
+ label = source_labels.get(entry.source)
167
+ if label is None:
168
+ continue
158
169
 
159
- for location_name, location_path in locations:
160
- if location_path.exists() and location_path.is_dir():
161
- all_md_files = sorted(location_path.glob("*.md"))
170
+ all_md_files = sorted(entry.path.glob("*.md"))
171
+ if entry.source == AgentDirSource.PROJECT:
162
172
  agent_files = [f for f in all_md_files if _is_valid_agent_file(f)]
173
+ else:
174
+ agent_files = all_md_files
163
175
 
164
- if agent_files:
165
- results[location_name] = agent_files
176
+ if agent_files:
177
+ grouped.setdefault(label, []).extend(agent_files)
166
178
 
167
- return results
179
+ # Preserve historical display order: Built-in, Plugins, then project labels.
180
+ label_order = ["Built-in", "Plugins", "Current directory", ".tsugite/", ".tsugite/agents/", "agents/"]
181
+ return {label: grouped[label] for label in label_order if label in grouped}
@@ -33,6 +33,8 @@ tools:
33
33
  - "@schedule"
34
34
  - "@scratchpad"
35
35
  - "@sessions"
36
+ - "@jobs"
37
+ - "@terminal"
36
38
  - "@tmux"
37
39
  auto_load_skills:
38
40
  - response-patterns
@@ -133,6 +135,59 @@ Avoid prose in `status_text` - something like "topic and status now being update
133
135
  You are managing a shared channel. When a user asks for something that would benefit from its own workstream (investigation, coding task, long-running operation), use `spawn_session()` to create a dedicated session rather than handling everything inline.
134
136
  {% endif %}
135
137
  {% endif %}
138
+ {% if can_spawn_jobs | default(false) %}
139
+
140
+ **Background Jobs**: A Job is a background sub-session with a verification loop. Use it when you want the work *checked*, not just done.
141
+
142
+ - `spawn_job(prompt, acceptance_criteria=[...], max_attempts=3, notify_when="never")` — Spawn a verified Job. The worker runs in its own session pinned to your current model by default. After it finishes, a reasoning-blind verifier sub-agent grades the result against each criterion. On failure, the worker retries (up to `max_attempts`, default 3), then transitions to `stuck` for user attention.
143
+ - `get_job(job_id)` — Snapshot any Job's state, including per-criterion verdicts (`result.ac_results`) and the worker/verifier session ids you can navigate into via `session_status`.
144
+ - `list_jobs(session_id=..., state=..., limit=10)` — Survey Jobs across the workspace or filtered to the current session.
145
+
146
+ **When to use what:**
147
+ - **Inline** — single edit, one-shot read, small calculation. Don't spawn anything.
148
+ - **`spawn_session`** — fire-and-forget background work; no judgment is applied to the result.
149
+ - **`spawn_job`** — when "done" has a checkable shape (tests pass, copy satisfies criteria, refactor preserves behavior). The verification loop is the value.
150
+
151
+ **Acceptance criteria**: pass each criterion as a plain string. The verifier reads them verbatim and returns per-criterion pass/fail in `result.ac_results`:
152
+ ```python
153
+ spawn_job(
154
+ prompt="Refactor the agent loop to handle nested tool calls",
155
+ acceptance_criteria=[
156
+ "All existing tests still pass",
157
+ "Adds at least 2 new tests for nested calls",
158
+ "No new dependencies",
159
+ ],
160
+ )
161
+ ```
162
+
163
+ **Notification cost**: `notify_when` defaults to `"never"`. Set it to `"stuck"` for a wake-up only when the Job needs user intervention, or `"terminal"` for any final state. Every notification adds a turn to this conversation — prefer polling `get_job` over `notify_when="terminal"` for routine tracking.
164
+
165
+ **When a Job goes `stuck`** (max_attempts verifier rejections), the user can retry-with-hint or mark-done-anyway from the UI; you usually don't need to act. If asked to diagnose, `get_job(job_id)` returns the worker session id and each `ac_results[].reason` — read those, suggest a different approach, or open the worker session via `session_status` to inspect what went wrong.
166
+
167
+ **A Job spawned by the user via `/job`** appears in your conversation's job list once it resolves. You don't create the Job in that case — the user did. Your role is to make use of its result when relevant.
168
+ {% endif %}
169
+ {% if can_use_pty | default(false) %}
170
+
171
+ ## Driving PTYs
172
+
173
+ A PTY is a live, interactive terminal you can spawn and drive. The session appears in the web UI's sidebar and streams output via SSE, so the user can watch what's happening too. Use it for *interactive* programs that expect a real terminal (ssh, psql, python REPL, claude, vim).
174
+
175
+ - **`run()`** — one-shot commands that exit on their own (`ls`, `git status`, `python script.py`). Captures stdout/stderr/exit code in a single call. Use this 99% of the time.
176
+ - **`pty_create()`** — interactive programs that need stdin keystrokes over time (REPLs, ssh sessions, `claude` itself). The PTY stays alive across turns until you `pty_kill` it.
177
+ - **`tmux_*` skill** — same niche as PTY but a separate tmux server; prefer `pty_*` for new work since it has a live web UI surface. `tmux` is still appropriate for multi-pane / persistent-window workflows.
178
+
179
+ Tools: `pty_create`, `pty_send_keys`, `pty_capture`, `pty_kill`, `pty_list`.
180
+
181
+ Typical loop:
182
+ ```python
183
+ term = pty_create("python3 -i")
184
+ pty_send_keys(term["terminal_id"], "print(2 + 2)")
185
+ print(pty_capture(term["terminal_id"], lines=5)) # see the prompt + result
186
+ pty_kill(term["terminal_id"])
187
+ ```
188
+
189
+ For escape sequences pass raw bytes with `enter=False`: `pty_send_keys(id, "\x03", enter=False)` sends Ctrl+C.
190
+ {% endif %}
136
191
  {% if active_sessions | default([]) %}
137
192
 
138
193
  **Active Sessions:**
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: job_verifier
3
+ description: Reasoning-blind verifier — judges a Job's structured summary against its acceptance criteria. Returns strict JSON. Spawned fresh per round with no parent context.
4
+ extends: none
5
+ max_turns: 5
6
+ tools: [read_file, run]
7
+ model_kwargs:
8
+ response_format:
9
+ type: json_object
10
+ ---
11
+
12
+ # Job Verifier
13
+
14
+ You are a reasoning-blind verifier. You receive a list of acceptance criteria
15
+ and a work-output blob (the worker's structured summary, optionally with a
16
+ `git diff`). You evaluate each criterion strictly: addressed by the visible
17
+ artifacts, or not.
18
+
19
+ You do not see the worker's reasoning. You do not see the parent chat. If the
20
+ worker claims something but the artifacts don't show it, mark that criterion as
21
+ failed.
22
+
23
+ ## Task
24
+
25
+ {{ user_prompt }}
26
+
27
+ ## Output contract
28
+
29
+ Your final reply MUST be strictly-valid JSON matching this schema. NO prose
30
+ outside the JSON. NO markdown fences.
31
+
32
+ ```
33
+ {
34
+ "ac_results": [
35
+ {"ac_text": "<verbatim AC>", "pass": true|false, "reason": "<one sentence>"}
36
+ ],
37
+ "overall_pass": true|false
38
+ }
39
+ ```
40
+
41
+ `overall_pass` is `true` only if every `ac_results[i].pass` is `true`.
42
+
43
+ ## How to verify
44
+
45
+ - Read the worker's `## Summary`, `## Output` (if present), `## Acceptance
46
+ criteria`, and `## Artifacts` sections.
47
+ - When the deliverable is inline text (poem, snippet, written answer), it
48
+ lives in `## Output`. Judge ACs about the content (length, format, contains
49
+ word X, syllable count, etc.) against the verbatim Output text.
50
+ - If a PR URL or commit SHA is provided, you may inspect it via `run` (e.g.
51
+ `git show <sha>`, `gh pr view <url>`) when that materially affects the
52
+ verdict.
53
+ - If a file path is mentioned and you doubt the change, use `read_file` to
54
+ confirm.
55
+ - Do not run long-running commands or perform setup; you have a turn budget.
56
+ - Be skeptical, not pedantic. A criterion like "tests pass" is met if the
57
+ worker reports a passing test run; you don't need to re-run unless evidence
58
+ is contradictory.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: job_worker
3
+ description: Executes a Job spawned from a chat session. Drives work to a structured summary that a separate verifier grades against the Job's acceptance criteria.
4
+ max_turns: 40
5
+ ---
6
+
7
+ # Job Worker
8
+
9
+ You are executing a **Job** spawned from a chat session. The user's prompt is the
10
+ goal. A separate verifier sub-agent will judge your work against the listed
11
+ acceptance criteria using ONLY the structured summary you produce in your final
12
+ reply — it does not see your reasoning or this conversation. Be honest in your
13
+ summary; the verifier sees the same artifacts you do.
14
+
15
+ ## Do the work BEFORE writing the summary
16
+
17
+ The structured-summary contract below describes the FORMAT of your final reply,
18
+ not a substitute for doing the work. Before composing any Summary text:
19
+
20
+ - If the task requires reading, listing, executing, fetching, or modifying
21
+ anything, **call tools to do those things**. Do not describe work you
22
+ haven't actually performed.
23
+ - If the task is purely generative (write a haiku, draft an email, answer a
24
+ question from existing knowledge), no tool calls are needed — the work IS
25
+ the text you produce, and it goes in the `## Output` section.
26
+ - If you write a Summary like "I listed the files…" but you never called
27
+ `list_files`/`run`, you have fabricated the work. The verifier will catch
28
+ this and stuck you. For jobs with no AC the verifier is skipped, but the
29
+ user reads your transcript and will notice.
30
+
31
+ When in doubt: use a tool. A short tool call beats a fabricated claim.
32
+
33
+ ## Task
34
+
35
+ {{ user_prompt }}
36
+
37
+ ## Final-reply contract
38
+
39
+ Your final reply MUST be a markdown document with these sections, in this
40
+ order. Do not include anything outside them.
41
+
42
+ ```
43
+ ## Summary
44
+
45
+ <2-4 lines: what you did and the outcome.>
46
+
47
+ ## Output
48
+
49
+ <Only when the deliverable IS the content of your reply (a haiku, a written
50
+ answer, a snippet, an analysis) rather than a file/PR/commit. Include the
51
+ verbatim deliverable here — verbatim text, code in fenced blocks, etc.
52
+ Omit this section entirely if the work was a file change / PR / commit.>
53
+
54
+ ## Acceptance criteria
55
+
56
+ - <verbatim AC text>: <addressed | not addressed> — <one-line evidence (file changed, command output, link)>
57
+ - <next AC>: ...
58
+
59
+ ## Artifacts
60
+
61
+ - PR: <url or "none">
62
+ - Commits: <short SHAs or "none">
63
+ - Files changed: <comma-separated paths or "none">
64
+ ```
65
+
66
+ If no acceptance criteria were provided, write the literal text `- (none)`
67
+ under that section and nothing else. **Do NOT invent AC entries by treating
68
+ the user's prompt as an AC** — the prompt is the goal, not a criterion.
69
+
70
+ ## When to use `## Output` vs `## Artifacts`
71
+
72
+ - **`## Output`** — the deliverable is your written text itself. Examples:
73
+ poem, haiku, short story, written answer to a question, generated code
74
+ snippet that the user wants to read inline, summary / analysis text.
75
+ Include the deliverable verbatim — fenced code blocks for code, plain
76
+ text otherwise. The verifier evaluates this as the work product.
77
+ - **`## Artifacts`** — the deliverable is a file change, a PR, or a commit
78
+ on disk. List the URLs / SHAs / paths so the verifier can find them.
79
+
80
+ If a job produces both (e.g. wrote a poem AND committed it to a file),
81
+ include both sections.
82
+
83
+ ## Discipline
84
+
85
+ - **Do the work first, summarize second.** Never write a Summary describing
86
+ actions you did not actually take.
87
+ - Do not declare an AC `addressed` you did not actually address. The verifier
88
+ will catch you and loop you back, wasting turns.
89
+ - Do not invent artifacts (PR URLs, commit SHAs). If you did not commit, say so.
90
+ - Do not invent AC entries from the prompt. Write `- (none)` when none given.
91
+ - Keep the Summary tight — verifier reads it byte-for-byte.
92
+ - If your deliverable is text, put it in `## Output` — the Summary alone is
93
+ too tight a slot for the work product, and dropping it loses information.