subroutine 0.1.2__tar.gz → 0.1.3__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 (193) hide show
  1. {subroutine-0.1.2 → subroutine-0.1.3}/.github/workflows/ci.yml +10 -10
  2. {subroutine-0.1.2 → subroutine-0.1.3}/.github/workflows/release.yml +7 -7
  3. {subroutine-0.1.2 → subroutine-0.1.3}/CHANGELOG.md +16 -0
  4. {subroutine-0.1.2 → subroutine-0.1.3}/CONTRIBUTING.md +4 -0
  5. {subroutine-0.1.2 → subroutine-0.1.3}/PKG-INFO +13 -7
  6. {subroutine-0.1.2 → subroutine-0.1.3}/README.md +12 -6
  7. subroutine-0.1.3/SECURITY.md +74 -0
  8. {subroutine-0.1.2 → subroutine-0.1.3}/plugins/subroutine/.claude-plugin/plugin.json +1 -1
  9. subroutine-0.1.3/scripts/release.py +198 -0
  10. subroutine-0.1.3/tests/test_release_script.py +233 -0
  11. {subroutine-0.1.2 → subroutine-0.1.3}/.claude-plugin/marketplace.json +0 -0
  12. {subroutine-0.1.2 → subroutine-0.1.3}/.gitignore +0 -0
  13. {subroutine-0.1.2 → subroutine-0.1.3}/CLA.md +0 -0
  14. {subroutine-0.1.2 → subroutine-0.1.3}/LICENSE +0 -0
  15. {subroutine-0.1.2 → subroutine-0.1.3}/alembic.ini +0 -0
  16. {subroutine-0.1.2 → subroutine-0.1.3}/docs/errors.md +0 -0
  17. {subroutine-0.1.2 → subroutine-0.1.3}/docs/hosting.md +0 -0
  18. {subroutine-0.1.2 → subroutine-0.1.3}/plugins/subroutine/.mcp.json +0 -0
  19. {subroutine-0.1.2 → subroutine-0.1.3}/plugins/subroutine/skills/subroutine/SKILL.md +0 -0
  20. {subroutine-0.1.2 → subroutine-0.1.3}/pyproject.toml +0 -0
  21. {subroutine-0.1.2 → subroutine-0.1.3}/scripts/check_licences.py +0 -0
  22. {subroutine-0.1.2 → subroutine-0.1.3}/scripts/check_release_notes.py +0 -0
  23. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/__init__.py +0 -0
  24. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/__main__.py +0 -0
  25. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/addressing.py +0 -0
  26. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/__init__.py +0 -0
  27. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/admin.py +0 -0
  28. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/agenda.py +0 -0
  29. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/app.py +0 -0
  30. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/comments.py +0 -0
  31. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/concurrency.py +0 -0
  32. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/dependencies.py +0 -0
  33. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/documents.py +0 -0
  34. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/events.py +0 -0
  35. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/health.py +0 -0
  36. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/identity.py +0 -0
  37. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/meta.py +0 -0
  38. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/middleware.py +0 -0
  39. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/pagination.py +0 -0
  40. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/problems.py +0 -0
  41. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/projects.py +0 -0
  42. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/query.py +0 -0
  43. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/routing.py +0 -0
  44. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/schemas.py +0 -0
  45. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/security.py +0 -0
  46. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/shaping.py +0 -0
  47. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/subjects.py +0 -0
  48. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/tasks.py +0 -0
  49. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/tokens.py +0 -0
  50. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/users.py +0 -0
  51. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/api/workspaces.py +0 -0
  52. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/auth.py +0 -0
  53. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/__init__.py +0 -0
  54. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/main.py +0 -0
  55. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/personal.py +0 -0
  56. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/cli/topics.py +0 -0
  57. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/__init__.py +0 -0
  58. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/base.py +0 -0
  59. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/http.py +0 -0
  60. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/local.py +0 -0
  61. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/clients/opening.py +0 -0
  62. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/config.py +0 -0
  63. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/connections.py +0 -0
  64. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/context.py +0 -0
  65. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/credentials.py +0 -0
  66. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/__init__.py +0 -0
  67. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/backup.py +0 -0
  68. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/base.py +0 -0
  69. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrate.py +0 -0
  70. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/env.py +0 -0
  71. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/script.py.mako +0 -0
  72. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/0c8f7a7027e6_refs_are_workspace_sequential_integers.py +0 -0
  73. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/233f898a2bee_instance_timezone_workspace_timezone_.py +0 -0
  74. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/2fee457e5b0b_initial_schema.py +0 -0
  75. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/547fe53b263c_an_event_carries_the_subject_it_.py +0 -0
  76. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/migrations/versions/ea3e86ad12c4_workspace_slug_frees_on_soft_delete.py +0 -0
  77. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/mixins.py +0 -0
  78. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/__init__.py +0 -0
  79. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/activity.py +0 -0
  80. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/identity.py +0 -0
  81. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/project.py +0 -0
  82. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/system.py +0 -0
  83. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/vocabulary.py +0 -0
  84. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/models/work.py +0 -0
  85. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/seed.py +0 -0
  86. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/session.py +0 -0
  87. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/transfer.py +0 -0
  88. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/db/types.py +0 -0
  89. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/directory.py +0 -0
  90. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/__init__.py +0 -0
  91. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/agenda.py +0 -0
  92. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/authentication.py +0 -0
  93. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/authorization.py +0 -0
  94. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/bootstrap.py +0 -0
  95. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/capture.py +0 -0
  96. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/comments.py +0 -0
  97. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/dates.py +0 -0
  98. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/documents.py +0 -0
  99. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/durations.py +0 -0
  100. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/events.py +0 -0
  101. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/hierarchy.py +0 -0
  102. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/instances.py +0 -0
  103. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/links.py +0 -0
  104. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/local.py +0 -0
  105. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/mentions.py +0 -0
  106. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/ordering.py +0 -0
  107. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/paging.py +0 -0
  108. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/patch.py +0 -0
  109. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/projects.py +0 -0
  110. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/readiness.py +0 -0
  111. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/refs.py +0 -0
  112. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/schedule.py +0 -0
  113. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/scoping.py +0 -0
  114. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/search.py +0 -0
  115. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/selection.py +0 -0
  116. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/tags.py +0 -0
  117. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/tasks.py +0 -0
  118. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/text.py +0 -0
  119. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/tokens.py +0 -0
  120. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/users.py +0 -0
  121. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/versions.py +0 -0
  122. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/domain/workspaces.py +0 -0
  123. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/errors.py +0 -0
  124. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/fanout.py +0 -0
  125. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/__init__.py +0 -0
  126. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/protocol.py +0 -0
  127. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/session.py +0 -0
  128. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/mcp/tools.py +0 -0
  129. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/permissions.py +0 -0
  130. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/py.typed +0 -0
  131. {subroutine-0.1.2 → subroutine-0.1.3}/src/subroutine/views.py +0 -0
  132. {subroutine-0.1.2 → subroutine-0.1.3}/tests/api_support.py +0 -0
  133. {subroutine-0.1.2 → subroutine-0.1.3}/tests/conftest.py +0 -0
  134. {subroutine-0.1.2 → subroutine-0.1.3}/tests/sample_models.py +0 -0
  135. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_actor_discipline.py +0 -0
  136. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_agenda.py +0 -0
  137. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_app.py +0 -0
  138. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_authentication.py +0 -0
  139. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_comments.py +0 -0
  140. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_concurrency.py +0 -0
  141. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_documents.py +0 -0
  142. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_events.py +0 -0
  143. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_examples.py +0 -0
  144. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_meta.py +0 -0
  145. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_problems.py +0 -0
  146. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_projects.py +0 -0
  147. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_routing.py +0 -0
  148. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_shaping.py +0 -0
  149. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_tasks.py +0 -0
  150. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_tokens.py +0 -0
  151. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_workspaces.py +0 -0
  152. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_api_writability.py +0 -0
  153. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_auth.py +0 -0
  154. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_authentication.py +0 -0
  155. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_authorization.py +0 -0
  156. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_bootstrap.py +0 -0
  157. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_capture.py +0 -0
  158. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_cli_connections.py +0 -0
  159. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_cli_help.py +0 -0
  160. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_config.py +0 -0
  161. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_connections.py +0 -0
  162. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_context.py +0 -0
  163. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_dates.py +0 -0
  164. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_db_types.py +0 -0
  165. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_directory.py +0 -0
  166. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_documentation.py +0 -0
  167. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_durations.py +0 -0
  168. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_errors.py +0 -0
  169. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_fanout.py +0 -0
  170. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_fixtures.py +0 -0
  171. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_imports.py +0 -0
  172. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_instances.py +0 -0
  173. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_isolation.py +0 -0
  174. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_local_mode.py +0 -0
  175. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_mcp.py +0 -0
  176. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_migrations.py +0 -0
  177. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_models.py +0 -0
  178. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_multi_user.py +0 -0
  179. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_ordering.py +0 -0
  180. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_personal_path.py +0 -0
  181. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_plugin.py +0 -0
  182. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_reach.py +0 -0
  183. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_release_notes.py +0 -0
  184. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_schedule.py +0 -0
  185. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_scoping.py +0 -0
  186. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_seed.py +0 -0
  187. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_services.py +0 -0
  188. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_smoke.py +0 -0
  189. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_spec_endpoints.py +0 -0
  190. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_sqlite_concurrency.py +0 -0
  191. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_transfer.py +0 -0
  192. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_transport_equivalence.py +0 -0
  193. {subroutine-0.1.2 → subroutine-0.1.3}/tests/test_upgrade.py +0 -0
@@ -29,11 +29,11 @@ jobs:
29
29
  # Full history and tags, because the release check compares this commit's migration
30
30
  # head against the one at the most recent tag. A shallow clone has neither, and the
31
31
  # check would find no previous tag and cheerfully pass every time.
32
- - uses: actions/checkout@v4
32
+ - uses: actions/checkout@v7
33
33
  with:
34
34
  fetch-depth: 0
35
35
 
36
- - uses: actions/setup-python@v5
36
+ - uses: actions/setup-python@v7
37
37
  with:
38
38
  python-version: "3.12"
39
39
  cache: pip
@@ -67,14 +67,14 @@ jobs:
67
67
  runs-on: ubuntu-latest
68
68
 
69
69
  steps:
70
- - uses: actions/checkout@v4
70
+ - uses: actions/checkout@v7
71
71
  with:
72
72
  # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
73
73
  # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
74
74
  # install a package quietly claiming to be a version nobody released.
75
75
  fetch-depth: 0
76
76
 
77
- - uses: actions/setup-python@v5
77
+ - uses: actions/setup-python@v7
78
78
  with:
79
79
  python-version: "3.12"
80
80
  cache: pip
@@ -102,9 +102,9 @@ jobs:
102
102
  runs-on: ubuntu-latest
103
103
 
104
104
  steps:
105
- - uses: actions/checkout@v4
105
+ - uses: actions/checkout@v7
106
106
 
107
- - uses: actions/setup-node@v4
107
+ - uses: actions/setup-node@v7
108
108
  with:
109
109
  node-version: "22"
110
110
 
@@ -143,14 +143,14 @@ jobs:
143
143
  --health-retries 10
144
144
 
145
145
  steps:
146
- - uses: actions/checkout@v4
146
+ - uses: actions/checkout@v7
147
147
  with:
148
148
  # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
149
149
  # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
150
150
  # install a package quietly claiming to be a version nobody released.
151
151
  fetch-depth: 0
152
152
 
153
- - uses: actions/setup-python@v5
153
+ - uses: actions/setup-python@v7
154
154
  with:
155
155
  python-version: ${{ matrix.python-version }}
156
156
  cache: pip
@@ -177,14 +177,14 @@ jobs:
177
177
  runs-on: ubuntu-latest
178
178
 
179
179
  steps:
180
- - uses: actions/checkout@v4
180
+ - uses: actions/checkout@v7
181
181
  with:
182
182
  # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
183
183
  # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
184
184
  # install a package quietly claiming to be a version nobody released.
185
185
  fetch-depth: 0
186
186
 
187
- - uses: actions/setup-python@v5
187
+ - uses: actions/setup-python@v7
188
188
  with:
189
189
  python-version: "3.12"
190
190
  cache: pip
@@ -51,14 +51,14 @@ jobs:
51
51
  --health-retries 10
52
52
 
53
53
  steps:
54
- - uses: actions/checkout@v4
54
+ - uses: actions/checkout@v7
55
55
  with:
56
56
  # Tags, because the version is derived from them (`SR#234`). A shallow clone makes
57
57
  # the build warn and produce `0.1.dev1+g…` rather than fail, so this job would
58
58
  # install a package quietly claiming to be a version nobody released.
59
59
  fetch-depth: 0
60
60
 
61
- - uses: actions/setup-python@v5
61
+ - uses: actions/setup-python@v7
62
62
  with:
63
63
  python-version: "3.12"
64
64
  cache: pip
@@ -83,11 +83,11 @@ jobs:
83
83
  steps:
84
84
  # Full history and tags: `check_release_notes.py` compares this commit's migration head
85
85
  # against the head at the previous tag, and a shallow clone has neither.
86
- - uses: actions/checkout@v4
86
+ - uses: actions/checkout@v7
87
87
  with:
88
88
  fetch-depth: 0
89
89
 
90
- - uses: actions/setup-python@v5
90
+ - uses: actions/setup-python@v7
91
91
  with:
92
92
  python-version: "3.12"
93
93
 
@@ -137,7 +137,7 @@ jobs:
137
137
  - name: Release notes
138
138
  run: python scripts/check_release_notes.py
139
139
 
140
- - uses: actions/upload-artifact@v4
140
+ - uses: actions/upload-artifact@v7
141
141
  with:
142
142
  name: dist
143
143
  path: dist/
@@ -159,7 +159,7 @@ jobs:
159
159
  id-token: write
160
160
 
161
161
  steps:
162
- - uses: actions/download-artifact@v4
162
+ - uses: actions/download-artifact@v8
163
163
  with:
164
164
  name: dist
165
165
  path: dist/
@@ -186,7 +186,7 @@ jobs:
186
186
  id-token: write
187
187
 
188
188
  steps:
189
- - uses: actions/download-artifact@v4
189
+ - uses: actions/download-artifact@v8
190
190
  with:
191
191
  name: dist
192
192
  path: dist/
@@ -12,6 +12,22 @@ 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.3 — 2026-08-02
16
+
17
+ ### Fixed
18
+
19
+ - **The first install line no longer fails on the machine most people have.** `pip install
20
+ subroutine` outside a virtualenv is refused by Debian, Ubuntu and Fedora — PEP 668's
21
+ `externally-managed-environment` — and that was the first command in this README, the first
22
+ on the PyPI page, and the first thing anybody read. It is now `uv tool install subroutine`
23
+ or `pipx install subroutine`: they work on those systems, they put `subroutine` on your
24
+ `PATH` where an editor or an agent can launch it, and pipx is what Debian's own error
25
+ message tells you to use.
26
+
27
+ **This corrects the note at the end of 0.1.2 below**, which said `pip install subroutine`
28
+ remained right if you were only going to type commands yourself. That holds inside a
29
+ virtualenv you have activated, and nowhere else — which is not what it implied.
30
+
15
31
  ## 0.1.2 — 2026-08-02
16
32
 
17
33
  No migration notice: the schema head has not moved since 0.1.0. Nothing in the package itself
@@ -16,6 +16,10 @@ That will change once the shape has settled, and this file will say so when it d
16
16
  - **Telling us it does not fit.** If you tried it and stopped, we would rather know why than
17
17
  not. An issue saying "I wanted X and there was no way to do it" is worth more than a guess.
18
18
 
19
+ **A vulnerability is the exception, and please treat it as one.** An issue is public from the
20
+ moment it is filed, so it is the one report that must not start there —
21
+ [SECURITY.md](SECURITY.md) says where it should go instead.
22
+
19
23
  The rest of this file is kept for two reasons: an unsolicited pull request still needs the
20
24
  licence agreement below if it is ever taken, and the conventions are worth having if you are
21
25
  reading the code to decide whether to trust it.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subroutine
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -73,7 +73,7 @@ Three things you might want. Pick one; they compose.
73
73
  **A to-do list on your own machine.** Nothing to configure.
74
74
 
75
75
  ```console
76
- $ pip install subroutine
76
+ $ uv tool install subroutine # or: pipx install subroutine
77
77
  $ subroutine init
78
78
  $ subroutine add "Call the dentist before Sunday"
79
79
  $ subroutine today
@@ -84,7 +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
+ $ uv tool install subroutine # or: pipx install subroutine
88
88
  $ claude plugin marketplace add simonholliday/subroutine
89
89
  $ claude plugin install subroutine@subroutine
90
90
  ```
@@ -119,16 +119,20 @@ commands and `subroutine explain dates` covers the ideas behind them.
119
119
  Python 3.11+ and thirteen dependencies. Nothing to create, nothing to configure, no server to
120
120
  start — SQLite is the default and `subroutine init` makes it.
121
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:
122
+ **Install it as a tool**, because that is what it is an application, not a library. It puts
123
+ `subroutine` on your `PATH`, which is what lets an editor or an agent launch it. It is also the
124
+ only thing that works on a current Linux: Debian, Ubuntu and Fedora now refuse a bare
125
+ `pip install` outside a virtualenv and tell you to use pipx instead.
125
126
 
126
127
  ```console
127
128
  $ uv tool install subroutine
128
129
  $ pipx install subroutine # the same thing, if you have pipx rather than uv
129
130
  ```
130
131
 
131
- PostgreSQL when you outgrow SQLite the extra goes on whichever of the three you used:
132
+ `pip install subroutine` is still right *inside* a virtualenv you have activated embedding it
133
+ in something else, or working on it.
134
+
135
+ PostgreSQL when you outgrow SQLite — the extra goes on whichever you used:
132
136
 
133
137
  ```console
134
138
  $ uv tool install "subroutine[postgres]"
@@ -352,6 +356,8 @@ Named plainly, because a tool that overstates itself wastes your afternoon:
352
356
  - **[docs/errors.md](docs/errors.md)** — every error code the API can return. Generated from
353
357
  the registry, so it cannot drift from the code.
354
358
  - **`GET /v1/docs/agent`** — the guide an agent should read first, written for that reader.
359
+ - **[SECURITY.md](SECURITY.md)** — how to report a vulnerability privately, and what is in
360
+ scope. Not through an issue: an issue is public from the moment it is filed.
355
361
 
356
362
  The full specification — data model, API, permissions and agent design — is written but
357
363
  not yet published. It lands here once the API has settled enough to be worth reading.
@@ -28,7 +28,7 @@ Three things you might want. Pick one; they compose.
28
28
  **A to-do list on your own machine.** Nothing to configure.
29
29
 
30
30
  ```console
31
- $ pip install subroutine
31
+ $ uv tool install subroutine # or: pipx install subroutine
32
32
  $ subroutine init
33
33
  $ subroutine add "Call the dentist before Sunday"
34
34
  $ subroutine today
@@ -39,7 +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
+ $ uv tool install subroutine # or: pipx install subroutine
43
43
  $ claude plugin marketplace add simonholliday/subroutine
44
44
  $ claude plugin install subroutine@subroutine
45
45
  ```
@@ -74,16 +74,20 @@ commands and `subroutine explain dates` covers the ideas behind them.
74
74
  Python 3.11+ and thirteen dependencies. Nothing to create, nothing to configure, no server to
75
75
  start — SQLite is the default and `subroutine init` makes it.
76
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:
77
+ **Install it as a tool**, because that is what it is an application, not a library. It puts
78
+ `subroutine` on your `PATH`, which is what lets an editor or an agent launch it. It is also the
79
+ only thing that works on a current Linux: Debian, Ubuntu and Fedora now refuse a bare
80
+ `pip install` outside a virtualenv and tell you to use pipx instead.
80
81
 
81
82
  ```console
82
83
  $ uv tool install subroutine
83
84
  $ pipx install subroutine # the same thing, if you have pipx rather than uv
84
85
  ```
85
86
 
86
- PostgreSQL when you outgrow SQLite the extra goes on whichever of the three you used:
87
+ `pip install subroutine` is still right *inside* a virtualenv you have activated embedding it
88
+ in something else, or working on it.
89
+
90
+ PostgreSQL when you outgrow SQLite — the extra goes on whichever you used:
87
91
 
88
92
  ```console
89
93
  $ uv tool install "subroutine[postgres]"
@@ -307,6 +311,8 @@ Named plainly, because a tool that overstates itself wastes your afternoon:
307
311
  - **[docs/errors.md](docs/errors.md)** — every error code the API can return. Generated from
308
312
  the registry, so it cannot drift from the code.
309
313
  - **`GET /v1/docs/agent`** — the guide an agent should read first, written for that reader.
314
+ - **[SECURITY.md](SECURITY.md)** — how to report a vulnerability privately, and what is in
315
+ scope. Not through an issue: an issue is public from the moment it is filed.
310
316
 
311
317
  The full specification — data model, API, permissions and agent design — is written but
312
318
  not yet published. It lands here once the API has settled enough to be worth reading.
@@ -0,0 +1,74 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ **Please do not open an issue.** An issue is public from the moment it is filed, which for a
6
+ vulnerability is the one thing that must not happen first.
7
+
8
+ Use **[GitHub's private vulnerability reporting](https://github.com/simonholliday/subroutine/security/advisories/new)**
9
+ — the *Report a vulnerability* button on the Security tab. It is private between you and the
10
+ maintainer, it needs no email address on either side, and it keeps the whole conversation with
11
+ the eventual advisory.
12
+
13
+ If that is not available to you, write to **simon.holliday@protonmail.com** with `Subroutine` in
14
+ the subject.
15
+
16
+ **What helps:** the version (`subroutine --version`), whether it was reached over the HTTP API
17
+ or the CLI, and the smallest sequence that shows the problem. A proof of concept is welcome and
18
+ not required — a clear description of the flaw is worth more than a working exploit.
19
+
20
+ ## What to expect
21
+
22
+ Subroutine is maintained by one person, so these are honest intentions rather than a
23
+ service-level agreement:
24
+
25
+ - **An acknowledgement within a few days.** If you have heard nothing in a week, assume it went
26
+ astray and send a reminder.
27
+ - **An assessment, and the reasoning.** If it turns out not to be a vulnerability you will be
28
+ told why, rather than left waiting.
29
+ - **Credit in the advisory** unless you would rather not be named.
30
+
31
+ Please give a reasonable window before disclosing publicly. If a fix is taking longer than you
32
+ think reasonable, say so — a deadline you have stated is easier to work to than one nobody
33
+ mentioned.
34
+
35
+ ## What is in scope
36
+
37
+ The code in this repository, and the current release on PyPI.
38
+
39
+ Things that are worth reporting even though they may look like design decisions, because the
40
+ boundary is exactly where mistakes hide:
41
+
42
+ - **A credential doing more than it should.** A token that is wider than the person who issued
43
+ it, or that survives something meant to end it.
44
+ - **Reading or writing across a boundary** — another workspace, a private project, another
45
+ user's items.
46
+ - **Anything that reaches the database or the filesystem through an input** that was not
47
+ supposed to.
48
+ - **A refusal that discloses.** "Forbidden" where the answer should be "not found" tells a
49
+ stranger something exists; the API is written to avoid that and a place it does not is a bug.
50
+
51
+ ## What is not
52
+
53
+ - **Anything requiring an account you were given.** A member of a workspace can read that
54
+ workspace; that is the product working.
55
+ - **`serve` bound to a wider interface with `--insecure`.** It refuses without TLS in front of
56
+ it and says why; overriding that is a deliberate act.
57
+ - **A self-hosted instance's own configuration** — file permissions, a database on a shared
58
+ volume, a reverse proxy that terminates TLS and forwards over a network you do not trust.
59
+ - **Dependency advisories with no reachable path here.** Do report one if you can show the path.
60
+
61
+ ## Supported versions
62
+
63
+ **The most recent release**, which is the only version that gets a fix. Subroutine is `0.x`: it
64
+ is early, the surface is still moving, and pretending to maintain several lines at once would be
65
+ a promise nobody could keep. Upgrading is `uv tool upgrade subroutine` or `pipx upgrade
66
+ subroutine` — whichever you installed it with — and then `subroutine upgrade` for the database.
67
+
68
+ ## One thing this project cannot do for you
69
+
70
+ **Subroutine authenticates with bearer tokens and stores their SHA-256 hashes.** A token is
71
+ shown once, at creation, and is unrecoverable afterwards by design. Nothing here can tell you
72
+ what a token was, and nobody who has the database can either — but anybody who can *read* an
73
+ instance's database can read every item in it. The database is a secret. `docs/hosting.md` says
74
+ where it should live and what should be backing it up.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subroutine",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",
@@ -0,0 +1,198 @@
1
+ """Cut a release — one version, written everywhere it has to appear. Item ``#239``.
2
+
3
+ **The number is a judgement and stays a human's.** Nothing here guesses whether a change is a
4
+ patch or a minor; what it removes is the chance to decide 0.1.3 and then say it in only some of
5
+ the places. ``#234`` took ``pyproject.toml`` out of that list by deriving the package version
6
+ from the tag. Three remain and every one has to agree on a single commit:
7
+
8
+ 1. the git tag, which is what the package version is built from;
9
+ 2. ``plugin.json``, which Claude Code reads straight from a clone with no build step, so
10
+ nothing can derive it;
11
+ 3. the changelog heading, which is how a person finds out what they are upgrading into.
12
+
13
+ **Two of the first three releases needed a corrective commit for exactly this** — 0.1.0's
14
+ changelog still said *Unreleased*, and 0.1.2 was tagged with the manifest reading 0.1.1, which
15
+ CI refused. Neither was carelessness; both were one step in a sequence somebody was holding in
16
+ their head.
17
+
18
+ **It refuses rather than repairs.** A dirty tree, a tag that exists, a version that goes
19
+ backwards and a changelog with nothing unreleased in it are all reasons to stop and let a
20
+ person look, because each means the release is not the thing they think it is.
21
+
22
+ **It never pushes.** Publishing is outward-facing and belongs to whoever owns the repository;
23
+ this stops at a commit and a tag, and prints the two commands that finish the job.
24
+ """
25
+
26
+ import argparse
27
+ import datetime
28
+ import json
29
+ import pathlib
30
+ import re
31
+ import subprocess
32
+ import sys
33
+
34
+ #: The repository, resolved from this file rather than from the working directory — the script
35
+ #: is run from wherever somebody happens to be standing.
36
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
37
+
38
+ #: The two files that carry a version. The changelog's heading and the plugin's manifest; the
39
+ #: tag is made rather than written, and `pyproject.toml` no longer has one at all (`#234`).
40
+ CHANGELOG = ROOT / "CHANGELOG.md"
41
+ PLUGIN = ROOT / "plugins" / "subroutine" / ".claude-plugin" / "plugin.json"
42
+
43
+ #: What a version may look like. Deliberately narrow: three numbers, optionally a pre-release
44
+ #: suffix. A tag is not the place to discover that somebody typed `v0.1.3` or `0.1` — the first
45
+ #: would produce `vv0.1.3` and the second a version PyPI sorts in a way nobody expects.
46
+ VERSION = re.compile(r"^\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.]+)?$")
47
+
48
+ #: The heading this promotes. **An `Unreleased` section has to already exist**, which is the
49
+ #: point rather than a limitation: it means the changelog was written while the work was fresh,
50
+ #: by whoever did it, instead of at the moment of release by whoever is in a hurry. The
51
+ #: changelog's own preamble already describes the file this way.
52
+ UNRELEASED = re.compile(r"^##\s+Unreleased\b.*$", re.IGNORECASE | re.MULTILINE)
53
+
54
+
55
+ def main (argv: list[str] | None = None) -> int:
56
+ """Write ``version`` into every place a release names itself, then commit and tag it."""
57
+
58
+ parsed = _arguments(argv)
59
+ version = parsed.version
60
+
61
+ if not VERSION.match(version):
62
+ return _refuse(f"{version!r} is not a version. Write it as 1.2.3, with no leading 'v'.")
63
+
64
+ problem = _reasons_to_stop(version)
65
+
66
+ if problem is not None:
67
+ return _refuse(problem)
68
+
69
+ on = parsed.date or datetime.date.today().isoformat()
70
+ changelog = UNRELEASED.sub(f"## {version} — {on}", CHANGELOG.read_text(encoding="utf-8"), 1)
71
+
72
+ if parsed.dry_run:
73
+ print(f"Would release {version} ({on}):")
74
+ print(f" {CHANGELOG.name}: the Unreleased heading becomes '## {version} — {on}'")
75
+ print(f" {PLUGIN.name}: version becomes {version}")
76
+ print(f" commit both, then tag v{version}")
77
+
78
+ return 0
79
+
80
+ CHANGELOG.write_text(changelog, encoding="utf-8")
81
+ _write_plugin_version(version)
82
+
83
+ # **`check_release_notes.py` is deliberately not run here.** It compares this commit's
84
+ # migration head against the head at the most recent tag — which is the same comparison CI
85
+ # makes on every push to main, against the same previous tag. So a missing migration notice
86
+ # is already refused before anybody reaches this script, and running it again would couple
87
+ # two scripts for an answer that has been available since the commit that moved the head.
88
+ _git("add", str(CHANGELOG), str(PLUGIN))
89
+ _git("commit", "-m", f"Release {version}", "-m", f"See CHANGELOG.md for what {version} contains.")
90
+ _git("tag", "-a", f"v{version}", "-m", f"Subroutine {version}")
91
+
92
+ print(f"Committed and tagged v{version}. Nothing has been pushed. To publish:")
93
+ print(" git push")
94
+ print(f" git push origin v{version}")
95
+
96
+ return 0
97
+
98
+
99
+ def _arguments (argv: list[str] | None) -> argparse.Namespace:
100
+ """Read the version to cut, and the two options that exist for testing it."""
101
+
102
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
103
+ parser.add_argument("version", help="the release, as 1.2.3, with no leading 'v'")
104
+ parser.add_argument(
105
+ "--dry-run", action="store_true", help="say what would change and touch nothing"
106
+ )
107
+
108
+ # Passed in rather than always read off the clock, so a test can assert the heading it
109
+ # expects instead of building today's date twice and comparing a program to itself.
110
+ parser.add_argument("--date", help="the date to stamp, as YYYY-MM-DD. Defaults to today.")
111
+
112
+ return parser.parse_args(argv)
113
+
114
+
115
+ def _reasons_to_stop (version: str) -> str | None:
116
+ """Return why this release must not be cut, or ``None`` if there is no reason."""
117
+
118
+ if _git("status", "--porcelain").strip():
119
+ return "the working tree has changes. Commit or stash them, so the tag names a state."
120
+
121
+ if _git("tag", "--list", f"v{version}").strip():
122
+ return f"v{version} already exists. A released version is never re-cut — choose the next one."
123
+
124
+ if not UNRELEASED.search(CHANGELOG.read_text(encoding="utf-8")):
125
+ return (
126
+ f"{CHANGELOG.name} has no '## Unreleased' section, so this release would say "
127
+ f"nothing about itself. Write what changed under one, then cut it."
128
+ )
129
+
130
+ latest = _latest_version()
131
+ proposed = _ordered(version)
132
+ previous = _ordered(latest) if latest is not None else None
133
+
134
+ # **Only when both parse as plain numbers.** A pre-release suffix makes ordering a question
135
+ # with more than one defensible answer, and guessing it here would refuse a release
136
+ # somebody meant. Refusing what is unambiguously backwards is the whole value.
137
+ if proposed is not None and previous is not None and proposed <= previous:
138
+ return f"{version} is not ahead of {latest}, the most recent tag."
139
+
140
+ return None
141
+
142
+
143
+ def _ordered (version: str) -> tuple[int, ...] | None:
144
+ """Return a version as numbers for comparison, or ``None`` if it is not purely numeric."""
145
+
146
+ parts = version.split(".")
147
+
148
+ if len(parts) != 3 or not all(part.isdigit() for part in parts):
149
+ return None
150
+
151
+ return tuple(int(part) for part in parts)
152
+
153
+
154
+ def _latest_version () -> str | None:
155
+ """Return the newest release tag with its ``v`` removed, or ``None`` on a repository with none."""
156
+
157
+ found = subprocess.run(
158
+ ["git", "describe", "--tags", "--abbrev=0", "--match", "v*"],
159
+ cwd=ROOT, capture_output=True, text=True, check=False,
160
+ )
161
+
162
+ if found.returncode != 0 or not found.stdout.strip():
163
+ return None
164
+
165
+ return found.stdout.strip().removeprefix("v")
166
+
167
+
168
+ def _write_plugin_version (version: str) -> None:
169
+ """Set the plugin manifest's version, leaving every other key and the formatting alone.
170
+
171
+ Rewritten through ``json`` rather than by pattern, because a manifest that stops being
172
+ valid JSON is one nobody can install and the failure arrives at a stranger.
173
+ """
174
+
175
+ manifest = json.loads(PLUGIN.read_text(encoding="utf-8"))
176
+ manifest["version"] = version
177
+
178
+ PLUGIN.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
179
+
180
+
181
+ def _git (*arguments: str) -> str:
182
+ """Run one git command in the repository and return its output, raising if it fails."""
183
+
184
+ return subprocess.run(
185
+ ["git", *arguments], cwd=ROOT, capture_output=True, text=True, check=True
186
+ ).stdout
187
+
188
+
189
+ def _refuse (message: str) -> int:
190
+ """Say why the release is not being cut, and exit non-zero."""
191
+
192
+ print(f"Not releasing: {message}", file=sys.stderr)
193
+
194
+ return 1
195
+
196
+
197
+ if __name__ == "__main__":
198
+ raise SystemExit(main())