subroutine 0.1.0__tar.gz → 0.1.2__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 (190) hide show
  1. {subroutine-0.1.0 → subroutine-0.1.2}/.claude-plugin/marketplace.json +1 -1
  2. {subroutine-0.1.0 → subroutine-0.1.2}/.github/workflows/ci.yml +15 -0
  3. {subroutine-0.1.0 → subroutine-0.1.2}/.github/workflows/release.yml +23 -7
  4. {subroutine-0.1.0 → subroutine-0.1.2}/CHANGELOG.md +40 -0
  5. {subroutine-0.1.0 → subroutine-0.1.2}/PKG-INFO +26 -12
  6. {subroutine-0.1.0 → subroutine-0.1.2}/README.md +25 -11
  7. {subroutine-0.1.0 → subroutine-0.1.2}/plugins/subroutine/.claude-plugin/plugin.json +3 -3
  8. {subroutine-0.1.0 → subroutine-0.1.2}/plugins/subroutine/skills/subroutine/SKILL.md +22 -7
  9. {subroutine-0.1.0 → subroutine-0.1.2}/pyproject.toml +14 -2
  10. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/cli/personal.py +5 -15
  11. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/directory.py +41 -0
  12. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/mcp/tools.py +28 -6
  13. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_cli_connections.py +15 -3
  14. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_directory.py +57 -0
  15. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_mcp.py +37 -0
  16. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_plugin.py +45 -8
  17. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_reach.py +21 -3
  18. {subroutine-0.1.0 → subroutine-0.1.2}/.gitignore +0 -0
  19. {subroutine-0.1.0 → subroutine-0.1.2}/CLA.md +0 -0
  20. {subroutine-0.1.0 → subroutine-0.1.2}/CONTRIBUTING.md +0 -0
  21. {subroutine-0.1.0 → subroutine-0.1.2}/LICENSE +0 -0
  22. {subroutine-0.1.0 → subroutine-0.1.2}/alembic.ini +0 -0
  23. {subroutine-0.1.0 → subroutine-0.1.2}/docs/errors.md +0 -0
  24. {subroutine-0.1.0 → subroutine-0.1.2}/docs/hosting.md +0 -0
  25. {subroutine-0.1.0 → subroutine-0.1.2}/plugins/subroutine/.mcp.json +0 -0
  26. {subroutine-0.1.0 → subroutine-0.1.2}/scripts/check_licences.py +0 -0
  27. {subroutine-0.1.0 → subroutine-0.1.2}/scripts/check_release_notes.py +0 -0
  28. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/__init__.py +0 -0
  29. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/__main__.py +0 -0
  30. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/addressing.py +0 -0
  31. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/__init__.py +0 -0
  32. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/admin.py +0 -0
  33. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/agenda.py +0 -0
  34. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/app.py +0 -0
  35. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/comments.py +0 -0
  36. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/concurrency.py +0 -0
  37. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/dependencies.py +0 -0
  38. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/documents.py +0 -0
  39. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/events.py +0 -0
  40. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/health.py +0 -0
  41. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/identity.py +0 -0
  42. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/meta.py +0 -0
  43. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/middleware.py +0 -0
  44. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/pagination.py +0 -0
  45. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/problems.py +0 -0
  46. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/projects.py +0 -0
  47. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/query.py +0 -0
  48. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/routing.py +0 -0
  49. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/schemas.py +0 -0
  50. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/security.py +0 -0
  51. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/shaping.py +0 -0
  52. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/subjects.py +0 -0
  53. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/tasks.py +0 -0
  54. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/tokens.py +0 -0
  55. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/users.py +0 -0
  56. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/api/workspaces.py +0 -0
  57. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/auth.py +0 -0
  58. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/cli/__init__.py +0 -0
  59. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/cli/main.py +0 -0
  60. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/cli/topics.py +0 -0
  61. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/clients/__init__.py +0 -0
  62. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/clients/base.py +0 -0
  63. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/clients/http.py +0 -0
  64. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/clients/local.py +0 -0
  65. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/clients/opening.py +0 -0
  66. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/config.py +0 -0
  67. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/connections.py +0 -0
  68. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/context.py +0 -0
  69. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/credentials.py +0 -0
  70. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/__init__.py +0 -0
  71. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/backup.py +0 -0
  72. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/base.py +0 -0
  73. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrate.py +0 -0
  74. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/env.py +0 -0
  75. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/script.py.mako +0 -0
  76. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/versions/0c8f7a7027e6_refs_are_workspace_sequential_integers.py +0 -0
  77. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/versions/233f898a2bee_instance_timezone_workspace_timezone_.py +0 -0
  78. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/versions/2fee457e5b0b_initial_schema.py +0 -0
  79. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/versions/547fe53b263c_an_event_carries_the_subject_it_.py +0 -0
  80. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/migrations/versions/ea3e86ad12c4_workspace_slug_frees_on_soft_delete.py +0 -0
  81. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/mixins.py +0 -0
  82. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/__init__.py +0 -0
  83. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/activity.py +0 -0
  84. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/identity.py +0 -0
  85. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/project.py +0 -0
  86. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/system.py +0 -0
  87. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/vocabulary.py +0 -0
  88. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/models/work.py +0 -0
  89. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/seed.py +0 -0
  90. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/session.py +0 -0
  91. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/transfer.py +0 -0
  92. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/db/types.py +0 -0
  93. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/__init__.py +0 -0
  94. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/agenda.py +0 -0
  95. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/authentication.py +0 -0
  96. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/authorization.py +0 -0
  97. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/bootstrap.py +0 -0
  98. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/capture.py +0 -0
  99. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/comments.py +0 -0
  100. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/dates.py +0 -0
  101. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/documents.py +0 -0
  102. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/durations.py +0 -0
  103. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/events.py +0 -0
  104. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/hierarchy.py +0 -0
  105. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/instances.py +0 -0
  106. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/links.py +0 -0
  107. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/local.py +0 -0
  108. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/mentions.py +0 -0
  109. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/ordering.py +0 -0
  110. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/paging.py +0 -0
  111. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/patch.py +0 -0
  112. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/projects.py +0 -0
  113. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/readiness.py +0 -0
  114. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/refs.py +0 -0
  115. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/schedule.py +0 -0
  116. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/scoping.py +0 -0
  117. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/search.py +0 -0
  118. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/selection.py +0 -0
  119. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/tags.py +0 -0
  120. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/tasks.py +0 -0
  121. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/text.py +0 -0
  122. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/tokens.py +0 -0
  123. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/users.py +0 -0
  124. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/versions.py +0 -0
  125. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/domain/workspaces.py +0 -0
  126. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/errors.py +0 -0
  127. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/fanout.py +0 -0
  128. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/mcp/__init__.py +0 -0
  129. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/mcp/protocol.py +0 -0
  130. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/mcp/session.py +0 -0
  131. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/permissions.py +0 -0
  132. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/py.typed +0 -0
  133. {subroutine-0.1.0 → subroutine-0.1.2}/src/subroutine/views.py +0 -0
  134. {subroutine-0.1.0 → subroutine-0.1.2}/tests/api_support.py +0 -0
  135. {subroutine-0.1.0 → subroutine-0.1.2}/tests/conftest.py +0 -0
  136. {subroutine-0.1.0 → subroutine-0.1.2}/tests/sample_models.py +0 -0
  137. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_actor_discipline.py +0 -0
  138. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_agenda.py +0 -0
  139. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_app.py +0 -0
  140. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_authentication.py +0 -0
  141. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_comments.py +0 -0
  142. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_concurrency.py +0 -0
  143. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_documents.py +0 -0
  144. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_events.py +0 -0
  145. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_examples.py +0 -0
  146. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_meta.py +0 -0
  147. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_problems.py +0 -0
  148. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_projects.py +0 -0
  149. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_routing.py +0 -0
  150. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_shaping.py +0 -0
  151. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_tasks.py +0 -0
  152. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_tokens.py +0 -0
  153. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_workspaces.py +0 -0
  154. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_api_writability.py +0 -0
  155. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_auth.py +0 -0
  156. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_authentication.py +0 -0
  157. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_authorization.py +0 -0
  158. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_bootstrap.py +0 -0
  159. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_capture.py +0 -0
  160. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_cli_help.py +0 -0
  161. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_config.py +0 -0
  162. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_connections.py +0 -0
  163. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_context.py +0 -0
  164. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_dates.py +0 -0
  165. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_db_types.py +0 -0
  166. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_documentation.py +0 -0
  167. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_durations.py +0 -0
  168. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_errors.py +0 -0
  169. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_fanout.py +0 -0
  170. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_fixtures.py +0 -0
  171. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_imports.py +0 -0
  172. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_instances.py +0 -0
  173. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_isolation.py +0 -0
  174. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_local_mode.py +0 -0
  175. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_migrations.py +0 -0
  176. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_models.py +0 -0
  177. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_multi_user.py +0 -0
  178. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_ordering.py +0 -0
  179. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_personal_path.py +0 -0
  180. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_release_notes.py +0 -0
  181. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_schedule.py +0 -0
  182. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_scoping.py +0 -0
  183. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_seed.py +0 -0
  184. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_services.py +0 -0
  185. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_smoke.py +0 -0
  186. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_spec_endpoints.py +0 -0
  187. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_sqlite_concurrency.py +0 -0
  188. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_transfer.py +0 -0
  189. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_transport_equivalence.py +0 -0
  190. {subroutine-0.1.0 → subroutine-0.1.2}/tests/test_upgrade.py +0 -0
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "subroutine",
12
12
  "source": "./plugins/subroutine",
13
- "description": "Give Claude a shared, durable task list: file work, find what is ready, record what happened, and leave the next session something to read. Needs Subroutine itself installed separately 'pip install subroutine', then 'subroutine init'.",
13
+ "description": "Give Claude a shared, durable task list: file work, find what is ready, record what happened, and leave the next session something to read. Needs Subroutine itself installed separately and on your PATH \u2014 'uv tool install subroutine' or 'pipx install subroutine', then 'subroutine init'.",
14
14
  "author": {
15
15
  "name": "Simon Holliday",
16
16
  "email": "simon.holliday@protonmail.com"
@@ -68,6 +68,11 @@ jobs:
68
68
 
69
69
  steps:
70
70
  - uses: actions/checkout@v4
71
+ with:
72
+ # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
73
+ # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
74
+ # install a package quietly claiming to be a version nobody released.
75
+ fetch-depth: 0
71
76
 
72
77
  - uses: actions/setup-python@v5
73
78
  with:
@@ -139,6 +144,11 @@ jobs:
139
144
 
140
145
  steps:
141
146
  - uses: actions/checkout@v4
147
+ with:
148
+ # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
149
+ # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
150
+ # install a package quietly claiming to be a version nobody released.
151
+ fetch-depth: 0
142
152
 
143
153
  - uses: actions/setup-python@v5
144
154
  with:
@@ -168,6 +178,11 @@ jobs:
168
178
 
169
179
  steps:
170
180
  - uses: actions/checkout@v4
181
+ with:
182
+ # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
183
+ # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
184
+ # install a package quietly claiming to be a version nobody released.
185
+ fetch-depth: 0
171
186
 
172
187
  - uses: actions/setup-python@v5
173
188
  with:
@@ -52,6 +52,11 @@ jobs:
52
52
 
53
53
  steps:
54
54
  - uses: actions/checkout@v4
55
+ with:
56
+ # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
57
+ # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
58
+ # install a package quietly claiming to be a version nobody released.
59
+ fetch-depth: 0
55
60
 
56
61
  - uses: actions/setup-python@v5
57
62
  with:
@@ -100,20 +105,31 @@ jobs:
100
105
  - name: Check the metadata
101
106
  run: twine check dist/*
102
107
 
103
- # The tag says which release this is and `pyproject.toml` says what will be uploaded
104
- # under that name. If they disagree, the thing published is not the thing the tag names
105
- # and this is the last moment that mistake costs nothing.
106
- - name: The tag names the version being built
108
+ # **Two things, and only the second can now be wrong** (`SR#234`). The package derives its
109
+ # version from the tag, so the first comparison is close to tautological it is kept
110
+ # because it is not *quite* one: a shallow clone, a missing tag or a dirty tree all make
111
+ # the backend produce a `.devN+g…` version instead, and this is where that would be caught
112
+ # rather than at the upload. The plugin manifest is static JSON with no build step, so it
113
+ # is the half that a release can genuinely publish out of step with itself.
114
+ - name: The tag names what is being built and what the plugin claims
107
115
  if: startsWith(github.ref, 'refs/tags/')
108
116
  run: |
109
117
  set -euo pipefail
110
118
  tagged="${GITHUB_REF_NAME#v}"
111
- built="$(python -c 'import pathlib, tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
119
+
120
+ built="$(python -c 'import importlib.metadata; print(importlib.metadata.version("subroutine"))')"
112
121
  if [ "$tagged" != "$built" ]; then
113
- echo "the tag $GITHUB_REF_NAME does not name version $built" >&2
122
+ echo "the tag $GITHUB_REF_NAME built version $built expected $tagged" >&2
114
123
  exit 1
115
124
  fi
116
- echo "tag and version agree on $built"
125
+
126
+ declared="$(python -c 'import json, pathlib; print(json.loads(pathlib.Path("plugins/subroutine/.claude-plugin/plugin.json").read_text())["version"])')"
127
+ if [ "$tagged" != "$declared" ]; then
128
+ echo "the plugin manifest says $declared, and this release is $tagged" >&2
129
+ exit 1
130
+ fi
131
+
132
+ echo "tag, package and plugin all agree on $tagged"
117
133
 
118
134
  # A release that moves the schema has to say so in the changelog (SPEC.md §12.4a). CI
119
135
  # checks this on every commit; it is checked again here because this is the run where
@@ -12,6 +12,46 @@ The point of it is that you can *plan* a database upgrade instead of meeting one
12
12
  through installing something. See [docs/hosting.md](docs/hosting.md#upgrading) for what the
13
13
  upgrade involves.
14
14
 
15
+ ## 0.1.2 — 2026-08-02
16
+
17
+ No migration notice: the schema head has not moved since 0.1.0. Nothing in the package itself
18
+ changed — this release is the plugin and the documentation around installing it.
19
+
20
+ ### Fixed
21
+
22
+ - Installing the Claude Code plugin now gives you working tools without your having to supply
23
+ a path. Your editor launches `subroutine` itself, so it has to be on your `PATH` — and
24
+ `pip install` into a virtualenv satisfies "installed" while leaving every tool missing, with
25
+ no error at the point of the mistake. The plugin route now tells you to install it as a
26
+ tool, with `uv tool install subroutine` or `pipx install subroutine`, which is what puts the
27
+ command where something other than your shell can find it.
28
+ - The plugin's skill sent that failure the wrong way. It could not tell "not installed" from
29
+ "installed where the editor cannot see it" — both look the same from inside a session — so it
30
+ told people who had already installed it to install it again. It now names `claude mcp list`,
31
+ which is the one command that separates them.
32
+
33
+ `pip install subroutine` remains exactly right if you are only going to type commands
34
+ yourself.
35
+
36
+ ## 0.1.1 — 2026-08-02
37
+
38
+ No migration notice: the schema head has not moved since 0.1.0.
39
+
40
+ ### Fixed
41
+
42
+ - An agent working in a checkout marked for a different instance could not file anything.
43
+ Committing a `.subroutine` file is how a team says which project a repository belongs to, so
44
+ a colleague who cloned the repository and ran `subroutine init` had a marker naming a project
45
+ that was not on their own instance — and `subroutine_add` refused every call rather than
46
+ ignoring it, while the CLI beside it carried on. Reads were never affected. A renamed project
47
+ is now also followed by its id over the Model Context Protocol, as it already was on the
48
+ command line.
49
+
50
+ ### Changed
51
+
52
+ - The version is derived from the git tag rather than written into `pyproject.toml`, so what
53
+ the package reports and what was released cannot drift apart.
54
+
15
55
  ## 0.1.0 — 2026-08-01
16
56
 
17
57
  The first public release. Everything here is new, so this section says what exists rather than
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subroutine
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Project management for people and agents, in equal measure.
5
5
  Project-URL: Homepage, https://github.com/simonholliday/subroutine
6
6
  Project-URL: Issues, https://github.com/simonholliday/subroutine/issues
@@ -84,6 +84,7 @@ practice — it keeps the backlog, records what it did, and adopts Subroutine in
84
84
  are already working on. **You never have to learn the CLI.**
85
85
 
86
86
  ```console
87
+ $ uv tool install subroutine # or pipx — your editor starts it, so it must be on your PATH
87
88
  $ claude plugin marketplace add simonholliday/subroutine
88
89
  $ claude plugin install subroutine@subroutine
89
90
  ```
@@ -96,11 +97,8 @@ $ subroutine serve
96
97
  $ subroutine token create --title "CI" --scope task:read # a credential that can only read
97
98
  ```
98
99
 
99
- **`pip install subroutine` lands with the PyPI release, shortly.** Until then, install from
100
- here `pip install git+https://github.com/simonholliday/subroutine` which is what every
101
- command on this page was verified against. The full hosting recipe is in
102
- [docs/hosting.md](docs/hosting.md); `subroutine help` lists the commands and `subroutine explain
103
- dates` covers the ideas behind them.
100
+ The full hosting recipe is in [docs/hosting.md](docs/hosting.md); `subroutine help` lists the
101
+ commands and `subroutine explain dates` covers the ideas behind them.
104
102
 
105
103
  > **My context window ends. The instance does not.**
106
104
  >
@@ -119,10 +117,21 @@ dates` covers the ideas behind them.
119
117
  ## Install
120
118
 
121
119
  Python 3.11+ and thirteen dependencies. Nothing to create, nothing to configure, no server to
122
- start — SQLite is the default and `subroutine init` makes it. PostgreSQL when you outgrow it:
120
+ start — SQLite is the default and `subroutine init` makes it.
121
+
122
+ `pip install subroutine` is fine if you are only going to type commands yourself. **If an
123
+ editor or an agent is going to launch it, install it as a tool instead** — that is what puts
124
+ `subroutine` on your `PATH`, where something other than your shell can find it:
125
+
126
+ ```console
127
+ $ uv tool install subroutine
128
+ $ pipx install subroutine # the same thing, if you have pipx rather than uv
129
+ ```
130
+
131
+ PostgreSQL when you outgrow SQLite — the extra goes on whichever of the three you used:
123
132
 
124
133
  ```console
125
- $ pip install "subroutine[postgres]"
134
+ $ uv tool install "subroutine[postgres]"
126
135
  ```
127
136
 
128
137
  ## The shape of it
@@ -211,10 +220,15 @@ $ claude plugin marketplace add simonholliday/subroutine
211
220
  $ claude plugin install subroutine@subroutine
212
221
  ```
213
222
 
214
- It asks once for the path to the `subroutine` command give the absolute path if you
215
- installed into a virtualenv your editor does not activate and optionally for a connection
216
- and a token, which are only needed for somebody else's instance. The token is kept in your
217
- system keychain, never in a settings file.
223
+ **Your editor launches `subroutine` itself, so it has to be on your `PATH`** which is what
224
+ `uv tool install` and `pipx install` guarantee and a virtualenv does not. If the tools do not
225
+ appear afterwards, that is nearly always why, and `claude mcp list` says so in one line:
226
+ installing a plugin and starting its server are separate moments, and only the first one
227
+ reports. `/plugin configure subroutine` takes the absolute path when you would rather point at
228
+ a virtualenv than install it again.
229
+
230
+ It can also be given a connection and a token, both only needed for somebody else's instance.
231
+ The token is kept in your system keychain, never in a settings file.
218
232
 
219
233
  Without the plugin, or for another MCP client:
220
234
 
@@ -39,6 +39,7 @@ practice — it keeps the backlog, records what it did, and adopts Subroutine in
39
39
  are already working on. **You never have to learn the CLI.**
40
40
 
41
41
  ```console
42
+ $ uv tool install subroutine # or pipx — your editor starts it, so it must be on your PATH
42
43
  $ claude plugin marketplace add simonholliday/subroutine
43
44
  $ claude plugin install subroutine@subroutine
44
45
  ```
@@ -51,11 +52,8 @@ $ subroutine serve
51
52
  $ subroutine token create --title "CI" --scope task:read # a credential that can only read
52
53
  ```
53
54
 
54
- **`pip install subroutine` lands with the PyPI release, shortly.** Until then, install from
55
- here `pip install git+https://github.com/simonholliday/subroutine` which is what every
56
- command on this page was verified against. The full hosting recipe is in
57
- [docs/hosting.md](docs/hosting.md); `subroutine help` lists the commands and `subroutine explain
58
- dates` covers the ideas behind them.
55
+ The full hosting recipe is in [docs/hosting.md](docs/hosting.md); `subroutine help` lists the
56
+ commands and `subroutine explain dates` covers the ideas behind them.
59
57
 
60
58
  > **My context window ends. The instance does not.**
61
59
  >
@@ -74,10 +72,21 @@ dates` covers the ideas behind them.
74
72
  ## Install
75
73
 
76
74
  Python 3.11+ and thirteen dependencies. Nothing to create, nothing to configure, no server to
77
- start — SQLite is the default and `subroutine init` makes it. PostgreSQL when you outgrow it:
75
+ start — SQLite is the default and `subroutine init` makes it.
76
+
77
+ `pip install subroutine` is fine if you are only going to type commands yourself. **If an
78
+ editor or an agent is going to launch it, install it as a tool instead** — that is what puts
79
+ `subroutine` on your `PATH`, where something other than your shell can find it:
80
+
81
+ ```console
82
+ $ uv tool install subroutine
83
+ $ pipx install subroutine # the same thing, if you have pipx rather than uv
84
+ ```
85
+
86
+ PostgreSQL when you outgrow SQLite — the extra goes on whichever of the three you used:
78
87
 
79
88
  ```console
80
- $ pip install "subroutine[postgres]"
89
+ $ uv tool install "subroutine[postgres]"
81
90
  ```
82
91
 
83
92
  ## The shape of it
@@ -166,10 +175,15 @@ $ claude plugin marketplace add simonholliday/subroutine
166
175
  $ claude plugin install subroutine@subroutine
167
176
  ```
168
177
 
169
- It asks once for the path to the `subroutine` command give the absolute path if you
170
- installed into a virtualenv your editor does not activate and optionally for a connection
171
- and a token, which are only needed for somebody else's instance. The token is kept in your
172
- system keychain, never in a settings file.
178
+ **Your editor launches `subroutine` itself, so it has to be on your `PATH`** which is what
179
+ `uv tool install` and `pipx install` guarantee and a virtualenv does not. If the tools do not
180
+ appear afterwards, that is nearly always why, and `claude mcp list` says so in one line:
181
+ installing a plugin and starting its server are separate moments, and only the first one
182
+ reports. `/plugin configure subroutine` takes the absolute path when you would rather point at
183
+ a virtualenv than install it again.
184
+
185
+ It can also be given a connection and a token, both only needed for somebody else's instance.
186
+ The token is kept in your system keychain, never in a settings file.
173
187
 
174
188
  Without the plugin, or for another MCP client:
175
189
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subroutine",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Project management for people and agents, in equal measure. Wires this session to a Subroutine instance and carries the working practice for using it well.",
5
5
  "author": {
6
6
  "name": "Simon Holliday",
@@ -18,7 +18,7 @@
18
18
  "command": {
19
19
  "type": "string",
20
20
  "title": "The subroutine command",
21
- "description": "Usually just 'subroutine'. Give the full path if you installed it into a virtualenv your editor does not activate for example /home/you/.venvs/subroutine/bin/subroutine. Not installed yet? Run 'pip install subroutine' and then 'subroutine init' in a terminal, then come back here.",
21
+ "description": "Usually just 'subroutine'. Your editor launches it, so it has to be on your PATH \u2014 'uv tool install subroutine' or 'pipx install subroutine' put it there. Give the full path instead if it lives in a virtualenv your editor does not activate, for example /home/you/.venvs/subroutine/bin/subroutine. Not installed yet? Install it in a terminal, run 'subroutine init', then come back here.",
22
22
  "default": "subroutine"
23
23
  },
24
24
  "connection": {
@@ -30,7 +30,7 @@
30
30
  "token": {
31
31
  "type": "string",
32
32
  "title": "Agent token (optional)",
33
- "description": "A token issued for this agent, if you want it to have less access than you do 'subroutine token create --service-account claude --scope task:read'. Leave empty to let it act as you. Stored in your system keychain, never in a settings file.",
33
+ "description": "A token issued for this agent, if you want it to have less access than you do \u2014 'subroutine token create --service-account claude --scope task:read'. Leave empty to let it act as you. Stored in your system keychain, never in a settings file.",
34
34
  "sensitive": true,
35
35
  "default": ""
36
36
  }
@@ -9,14 +9,30 @@ Subroutine is a task and project tracker that a person and an agent use as equal
9
9
  write in it is attributed to you, addressable by a number, and still there after your context
10
10
  is gone — which is the whole reason to spend calls on it.
11
11
 
12
- **If the `subroutine_*` tools are not available, stop and say so.** The plugin configures them
13
- but does not install the program. Tell the user:
12
+ **If the `subroutine_*` tools are not available, stop and say so and do not guess which of the
13
+ two causes it is.** The plugin configures the tools but does not install the program, so either
14
+ it is not installed, or it is installed somewhere the editor cannot see. Both look identical
15
+ from here: no tools, and no error, because installing a plugin and starting its server are
16
+ separate moments and only the first one reports. One command tells them apart:
14
17
 
15
18
  ```
16
- pip install subroutine
19
+ claude mcp list
20
+ ```
21
+
22
+ `✘ Failed to connect` beside `plugin:subroutine:subroutine` means the program was not found on
23
+ the `PATH` the editor passes down — nearly always a virtualenv. Say so, and offer the two ways
24
+ out: install it as a tool so it is on the `PATH` for good, or point the plugin at the copy that
25
+ already exists.
26
+
27
+ ```
28
+ uv tool install subroutine # or: pipx install subroutine
17
29
  subroutine init
18
30
  ```
19
31
 
32
+ `/plugin configure subroutine` takes the absolute path instead — `<venv>/bin/subroutine` — for
33
+ somebody who would rather not install it twice. If the server is not listed at all, the plugin
34
+ itself is not installed or is disabled.
35
+
20
36
  **If the tools are there but every call fails, read what the failure says — it names the
21
37
  remedy.** This is the ordinary case on a fresh install and it is not the one above: the MCP
22
38
  server starts perfectly well against an instance nobody has created yet, so the tools appear
@@ -24,10 +40,9 @@ and then refuse. `no Subroutine instance has been set up here yet.` means the us
24
40
  `subroutine init` once; a schema message means `subroutine upgrade`. Pass the failure on
25
41
  verbatim rather than diagnosing it — the sentence is written for them.
26
42
 
27
- If it is installed but the tools still fail with nothing useful, it is almost certainly on a
28
- path Claude Code cannot see a virtualenv, usually. `/plugin configure subroutine` takes the
29
- full path to the `subroutine` command. Do not work around it by writing to a file instead; a
30
- tracker nobody can reach is worse than an honest failure.
43
+ **Whichever of these it is, do not work around it by writing to a file instead.** A tracker
44
+ nobody can reach is worse than an honest failure, and a to-do list in a scratch file is a
45
+ tracker nobody can reach.
31
46
 
32
47
  `GET /v1/docs/agent` on any served instance is the reference for the HTTP API. This page is
33
48
  about *when* to reach for it and what good practice looks like — it does not repeat the API.
@@ -1,10 +1,19 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling", "hatch-vcs"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "subroutine"
7
- version = "0.1.0"
7
+
8
+ # Derived from the git tag, never written here (`#234`). `v0.1.1` builds `0.1.1`; anything
9
+ # further along builds a `.devN+g<sha>` version, which PyPI refuses — so an untagged commit
10
+ # cannot be published even by accident. `subroutine.__version__` already read this back
11
+ # through `importlib.metadata`, so nothing at runtime changed.
12
+ #
13
+ # **The cost is that a shallow clone lies rather than fails.** Without tags the backend warns
14
+ # and produces `0.1.dev1+g…`, so every workflow job that installs this package checks out with
15
+ # `fetch-depth: 0`.
16
+ dynamic = ["version"]
8
17
  description = "Project management for people and agents, in equal measure."
9
18
  readme = "README.md"
10
19
  requires-python = ">=3.11"
@@ -41,6 +50,9 @@ dependencies = [
41
50
  "uvicorn[standard]>=0.30",
42
51
  ]
43
52
 
53
+ [tool.hatch.version]
54
+ source = "vcs"
55
+
44
56
  [project.optional-dependencies]
45
57
  postgres = ["psycopg[binary]>=3.1"]
46
58
  dev = [
@@ -2606,26 +2606,16 @@ def register (
2606
2606
  def _project_named_by (world: World, marker: subroutine.directory.Marker) -> str | None:
2607
2607
  """Return the current key of the project a marker names, or ``None`` if there is none.
2608
2608
 
2609
- By id where the marker carries one, because that is the half that survives a rename;
2610
- by key otherwise, which is every marker written before `#177` including the one in
2611
- this repository. A marker that predates the change must go on working, or the upgrade
2612
- is the outage.
2609
+ The matching itself is `subroutine.directory.resolve`, which is shared with the MCP
2610
+ server this half is only the fetching, because that is the part that needs a world
2611
+ to fetch through. It was one function here until `#232` found the other surface had no
2612
+ equivalent at all.
2613
2613
  """
2614
2614
 
2615
2615
  where = world.writing_to()
2616
2616
  found = where.client.projects(workspace=_writing_workspace(world))
2617
2617
 
2618
- if marker.project_id is not None:
2619
- for row in found:
2620
- if str(row.id) == marker.project_id:
2621
- return row.key
2622
-
2623
- if marker.project is not None:
2624
- for row in found:
2625
- if row.key.upper() == marker.project.upper():
2626
- return row.key
2627
-
2628
- return None
2618
+ return subroutine.directory.resolve(marker, found)
2629
2619
 
2630
2620
  @app.command(hidden=not _worth_showing(settings))
2631
2621
  def connections () -> None:
@@ -81,6 +81,47 @@ class Marker(typing.NamedTuple):
81
81
  return f"{named or 'nothing'}, from {self.path}"
82
82
 
83
83
 
84
+ class Named(typing.Protocol):
85
+ """The two fields a marker is resolved against, on whatever a client hands back."""
86
+
87
+ @property
88
+ def id (self) -> typing.Any:
89
+ """The project's permanent identifier."""
90
+
91
+ @property
92
+ def key (self) -> str:
93
+ """The project's current key."""
94
+
95
+
96
+ def resolve (marker: Marker, projects: typing.Iterable[Named]) -> str | None:
97
+ """Return the current key of the project a marker names, or ``None`` if there is none.
98
+
99
+ By id where the marker carries one, because that is the half that survives a rename
100
+ (`#177`); by key otherwise, which is every marker written before that change — including
101
+ the one in this repository. A marker that predates it must go on working, or the upgrade
102
+ is the outage.
103
+
104
+ **Returning ``None`` is an answer, not a failure** (`#166`). A marker is advisory context
105
+ written by a machine, so a checkout marked for one instance must not stop work being filed
106
+ against another — the caller reports that it was ignored and carries on. This lives here,
107
+ client-free and taking rows rather than fetching them, because both surfaces need the same
108
+ answer and the CLI had the only copy: `subroutine_add` passed the marker's key straight to
109
+ the server and refused whenever it did not exist there (`#228`'s neighbour, `#232`).
110
+ """
111
+
112
+ if marker.project_id is not None:
113
+ for row in projects:
114
+ if str(row.id) == marker.project_id:
115
+ return row.key
116
+
117
+ if marker.project is not None:
118
+ for row in projects:
119
+ if row.key.upper() == marker.project.upper():
120
+ return row.key
121
+
122
+ return None
123
+
124
+
84
125
  def find (start: pathlib.Path | None = None) -> Marker | None:
85
126
  """Return the nearest marker at or above ``start``, or ``None``.
86
127
 
@@ -472,27 +472,49 @@ def _added (
472
472
  # need it restarted — which is the one thing an agent cannot do to itself.
473
473
  marker = subroutine.directory.find()
474
474
  line = _text(arguments, "text") or ""
475
- filed = (
476
- marker.project
477
- if marker is not None
478
- and marker.project is not None
475
+ workspace = _text(arguments, "workspace")
476
+
477
+ consulted = (
478
+ marker is not None
479
+ and (marker.project is not None or marker.project_id is not None)
479
480
  and not subroutine.domain.capture.names_a_project(line)
481
+ )
482
+
483
+ # **Resolved against this instance, never passed through** (`#232`). The marker's key went
484
+ # straight to the server until 0.1.0, so a checkout marked for somebody else's instance —
485
+ # which is what committing this file is *for* — refused every `subroutine_add` with "there
486
+ # is no project 'SR' here", while the CLI beside it filed the task and said it had ignored
487
+ # the marker. `#166` settled that the marker is advisory; only one surface implemented it.
488
+ # Resolving also buys `#177`: a renamed project is followed by id, which this never did.
489
+ filed = (
490
+ subroutine.directory.resolve(marker, client.projects(workspace=workspace))
491
+ if consulted and marker is not None
480
492
  else None
481
493
  )
482
494
 
483
495
  captured = client.capture(
484
496
  text=line,
485
- workspace=_text(arguments, "workspace"),
497
+ workspace=workspace,
486
498
  type=_text(arguments, "type"),
487
499
  project=filed,
488
500
  )
489
501
  answer = "Added " + _line(captured.task)
490
502
 
491
503
  # Said out loud for the same reason the CLI says it: nobody typed it, and an agent that
492
- # cannot see where its work went cannot tell a person either.
504
+ # cannot see where its work went cannot tell a person either. That argument applies just
505
+ # as much when the marker was *not* used — more so, because the agent is then holding a
506
+ # repository whose file says one thing and an instance that says another.
493
507
  if filed is not None:
494
508
  answer = f"{answer}\n in {filed}, from {subroutine.directory.FILE_NAME}"
495
509
 
510
+ elif consulted and marker is not None:
511
+ shown = marker.project or marker.project_id
512
+
513
+ answer = (
514
+ f"{answer}\n {subroutine.directory.FILE_NAME} here names {shown!r}, which is not "
515
+ f"on this instance. Ignoring it."
516
+ )
517
+
496
518
  # Both halves of §6.13's obligation, and `#135` is why the second one is here: an agent is
497
519
  # the caller most likely to have written something it believes was understood, and telling
498
520
  # it only what was *left* answers the rarer question.
@@ -21,6 +21,7 @@ import subprocess
21
21
  import sys
22
22
  import time
23
23
  import typing
24
+ import zoneinfo
24
25
 
25
26
  import httpx
26
27
  import pytest
@@ -30,6 +31,13 @@ import typer.testing
30
31
  import subroutine.cli.main
31
32
  import subroutine.credentials
32
33
 
34
+ #: The zone every instance in this file is created in, named once because a test that asks
35
+ #: what day it is has to ask *this* clock rather than the machine's (`#233`). Deliberately
36
+ #: not UTC: a zone that is offset from the runner's is what makes a whole-day expiry mean
37
+ #: something to assert.
38
+ INSTANCE_ZONE = "Europe/London"
39
+
40
+
33
41
  #: How long to wait for a freshly started server before giving up on it.
34
42
  STARTUP_TIMEOUT_SECONDS = 20.0
35
43
 
@@ -47,7 +55,7 @@ def home (tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Pa
47
55
  if name.startswith(("SUBROUTINE_TOKEN", "SUBROUTINE_WORKSPACE", "SUBROUTINE_CONNECTION")):
48
56
  monkeypatch.delenv(name, raising=False)
49
57
 
50
- monkeypatch.setenv("SUBROUTINE_DEFAULT_TIMEZONE", "Europe/London")
58
+ monkeypatch.setenv("SUBROUTINE_DEFAULT_TIMEZONE", INSTANCE_ZONE)
51
59
 
52
60
  return root
53
61
 
@@ -121,7 +129,7 @@ def served (tmp_path: pathlib.Path) -> typing.Iterator[Remote]:
121
129
  "XDG_CONFIG_HOME": str(root / "config"),
122
130
  "XDG_DATA_HOME": str(root / "data"),
123
131
  "XDG_STATE_HOME": str(root / "state"),
124
- "SUBROUTINE_DEFAULT_TIMEZONE": "Europe/London",
132
+ "SUBROUTINE_DEFAULT_TIMEZONE": INSTANCE_ZONE,
125
133
  }
126
134
 
127
135
  for name in list(environment):
@@ -929,7 +937,11 @@ def test_an_expiry_names_a_whole_day_and_the_token_lives_through_it (
929
937
 
930
938
  run("init")
931
939
 
932
- today = datetime.date.today().isoformat()
940
+ # **The instance's today, not the machine's** (`#233`). The runner is UTC and the instance
941
+ # is an hour ahead of it, so between 23:00 and midnight `date.today()` names a day that
942
+ # ended here an hour ago — and the token issued "through today" is already expired when the
943
+ # listing is read. One hour a night, every night the offset is not zero.
944
+ today = datetime.datetime.now(zoneinfo.ZoneInfo(INSTANCE_ZONE)).date().isoformat()
933
945
  issued = run("token", "create", "--title", "through today", "--expires", today)
934
946
  secret = next(word for word in issued.output.split() if word.startswith("sr_"))
935
947