subroutine 0.1.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 (190) hide show
  1. subroutine-0.1.0/.claude-plugin/marketplace.json +29 -0
  2. subroutine-0.1.0/.github/workflows/ci.yml +221 -0
  3. subroutine-0.1.0/.github/workflows/release.yml +178 -0
  4. subroutine-0.1.0/.gitignore +53 -0
  5. subroutine-0.1.0/CHANGELOG.md +76 -0
  6. subroutine-0.1.0/CLA.md +78 -0
  7. subroutine-0.1.0/CONTRIBUTING.md +131 -0
  8. subroutine-0.1.0/LICENSE +661 -0
  9. subroutine-0.1.0/PKG-INFO +362 -0
  10. subroutine-0.1.0/README.md +317 -0
  11. subroutine-0.1.0/alembic.ini +43 -0
  12. subroutine-0.1.0/docs/errors.md +132 -0
  13. subroutine-0.1.0/docs/hosting.md +611 -0
  14. subroutine-0.1.0/plugins/subroutine/.claude-plugin/plugin.json +38 -0
  15. subroutine-0.1.0/plugins/subroutine/.mcp.json +11 -0
  16. subroutine-0.1.0/plugins/subroutine/skills/subroutine/SKILL.md +186 -0
  17. subroutine-0.1.0/pyproject.toml +157 -0
  18. subroutine-0.1.0/scripts/check_licences.py +264 -0
  19. subroutine-0.1.0/scripts/check_release_notes.py +267 -0
  20. subroutine-0.1.0/src/subroutine/__init__.py +25 -0
  21. subroutine-0.1.0/src/subroutine/__main__.py +12 -0
  22. subroutine-0.1.0/src/subroutine/addressing.py +49 -0
  23. subroutine-0.1.0/src/subroutine/api/__init__.py +11 -0
  24. subroutine-0.1.0/src/subroutine/api/admin.py +117 -0
  25. subroutine-0.1.0/src/subroutine/api/agenda.py +115 -0
  26. subroutine-0.1.0/src/subroutine/api/app.py +171 -0
  27. subroutine-0.1.0/src/subroutine/api/comments.py +303 -0
  28. subroutine-0.1.0/src/subroutine/api/concurrency.py +131 -0
  29. subroutine-0.1.0/src/subroutine/api/dependencies.py +85 -0
  30. subroutine-0.1.0/src/subroutine/api/documents.py +657 -0
  31. subroutine-0.1.0/src/subroutine/api/events.py +204 -0
  32. subroutine-0.1.0/src/subroutine/api/health.py +76 -0
  33. subroutine-0.1.0/src/subroutine/api/identity.py +194 -0
  34. subroutine-0.1.0/src/subroutine/api/meta.py +797 -0
  35. subroutine-0.1.0/src/subroutine/api/middleware.py +86 -0
  36. subroutine-0.1.0/src/subroutine/api/pagination.py +304 -0
  37. subroutine-0.1.0/src/subroutine/api/problems.py +319 -0
  38. subroutine-0.1.0/src/subroutine/api/projects.py +415 -0
  39. subroutine-0.1.0/src/subroutine/api/query.py +156 -0
  40. subroutine-0.1.0/src/subroutine/api/routing.py +181 -0
  41. subroutine-0.1.0/src/subroutine/api/schemas.py +87 -0
  42. subroutine-0.1.0/src/subroutine/api/security.py +140 -0
  43. subroutine-0.1.0/src/subroutine/api/shaping.py +279 -0
  44. subroutine-0.1.0/src/subroutine/api/subjects.py +66 -0
  45. subroutine-0.1.0/src/subroutine/api/tasks.py +767 -0
  46. subroutine-0.1.0/src/subroutine/api/tokens.py +298 -0
  47. subroutine-0.1.0/src/subroutine/api/users.py +126 -0
  48. subroutine-0.1.0/src/subroutine/api/workspaces.py +398 -0
  49. subroutine-0.1.0/src/subroutine/auth.py +201 -0
  50. subroutine-0.1.0/src/subroutine/cli/__init__.py +6 -0
  51. subroutine-0.1.0/src/subroutine/cli/main.py +2087 -0
  52. subroutine-0.1.0/src/subroutine/cli/personal.py +3890 -0
  53. subroutine-0.1.0/src/subroutine/cli/topics.py +214 -0
  54. subroutine-0.1.0/src/subroutine/clients/__init__.py +6 -0
  55. subroutine-0.1.0/src/subroutine/clients/base.py +538 -0
  56. subroutine-0.1.0/src/subroutine/clients/http.py +943 -0
  57. subroutine-0.1.0/src/subroutine/clients/local.py +1357 -0
  58. subroutine-0.1.0/src/subroutine/clients/opening.py +37 -0
  59. subroutine-0.1.0/src/subroutine/config.py +736 -0
  60. subroutine-0.1.0/src/subroutine/connections.py +430 -0
  61. subroutine-0.1.0/src/subroutine/context.py +295 -0
  62. subroutine-0.1.0/src/subroutine/credentials.py +350 -0
  63. subroutine-0.1.0/src/subroutine/db/__init__.py +1 -0
  64. subroutine-0.1.0/src/subroutine/db/backup.py +1045 -0
  65. subroutine-0.1.0/src/subroutine/db/base.py +27 -0
  66. subroutine-0.1.0/src/subroutine/db/migrate.py +294 -0
  67. subroutine-0.1.0/src/subroutine/db/migrations/env.py +164 -0
  68. subroutine-0.1.0/src/subroutine/db/migrations/script.py.mako +35 -0
  69. subroutine-0.1.0/src/subroutine/db/migrations/versions/0c8f7a7027e6_refs_are_workspace_sequential_integers.py +215 -0
  70. subroutine-0.1.0/src/subroutine/db/migrations/versions/233f898a2bee_instance_timezone_workspace_timezone_.py +58 -0
  71. subroutine-0.1.0/src/subroutine/db/migrations/versions/2fee457e5b0b_initial_schema.py +615 -0
  72. subroutine-0.1.0/src/subroutine/db/migrations/versions/547fe53b263c_an_event_carries_the_subject_it_.py +195 -0
  73. subroutine-0.1.0/src/subroutine/db/migrations/versions/ea3e86ad12c4_workspace_slug_frees_on_soft_delete.py +45 -0
  74. subroutine-0.1.0/src/subroutine/db/mixins.py +148 -0
  75. subroutine-0.1.0/src/subroutine/db/models/__init__.py +17 -0
  76. subroutine-0.1.0/src/subroutine/db/models/activity.py +147 -0
  77. subroutine-0.1.0/src/subroutine/db/models/identity.py +262 -0
  78. subroutine-0.1.0/src/subroutine/db/models/project.py +165 -0
  79. subroutine-0.1.0/src/subroutine/db/models/system.py +53 -0
  80. subroutine-0.1.0/src/subroutine/db/models/vocabulary.py +172 -0
  81. subroutine-0.1.0/src/subroutine/db/models/work.py +477 -0
  82. subroutine-0.1.0/src/subroutine/db/seed.py +514 -0
  83. subroutine-0.1.0/src/subroutine/db/session.py +159 -0
  84. subroutine-0.1.0/src/subroutine/db/transfer.py +313 -0
  85. subroutine-0.1.0/src/subroutine/db/types.py +157 -0
  86. subroutine-0.1.0/src/subroutine/directory.py +170 -0
  87. subroutine-0.1.0/src/subroutine/domain/__init__.py +9 -0
  88. subroutine-0.1.0/src/subroutine/domain/agenda.py +227 -0
  89. subroutine-0.1.0/src/subroutine/domain/authentication.py +442 -0
  90. subroutine-0.1.0/src/subroutine/domain/authorization.py +599 -0
  91. subroutine-0.1.0/src/subroutine/domain/bootstrap.py +215 -0
  92. subroutine-0.1.0/src/subroutine/domain/capture.py +536 -0
  93. subroutine-0.1.0/src/subroutine/domain/comments.py +393 -0
  94. subroutine-0.1.0/src/subroutine/domain/dates.py +317 -0
  95. subroutine-0.1.0/src/subroutine/domain/documents.py +571 -0
  96. subroutine-0.1.0/src/subroutine/domain/durations.py +191 -0
  97. subroutine-0.1.0/src/subroutine/domain/events.py +198 -0
  98. subroutine-0.1.0/src/subroutine/domain/hierarchy.py +213 -0
  99. subroutine-0.1.0/src/subroutine/domain/instances.py +55 -0
  100. subroutine-0.1.0/src/subroutine/domain/links.py +552 -0
  101. subroutine-0.1.0/src/subroutine/domain/local.py +291 -0
  102. subroutine-0.1.0/src/subroutine/domain/mentions.py +211 -0
  103. subroutine-0.1.0/src/subroutine/domain/ordering.py +326 -0
  104. subroutine-0.1.0/src/subroutine/domain/paging.py +52 -0
  105. subroutine-0.1.0/src/subroutine/domain/patch.py +43 -0
  106. subroutine-0.1.0/src/subroutine/domain/projects.py +629 -0
  107. subroutine-0.1.0/src/subroutine/domain/readiness.py +153 -0
  108. subroutine-0.1.0/src/subroutine/domain/refs.py +222 -0
  109. subroutine-0.1.0/src/subroutine/domain/schedule.py +391 -0
  110. subroutine-0.1.0/src/subroutine/domain/scoping.py +194 -0
  111. subroutine-0.1.0/src/subroutine/domain/search.py +60 -0
  112. subroutine-0.1.0/src/subroutine/domain/selection.py +198 -0
  113. subroutine-0.1.0/src/subroutine/domain/tags.py +273 -0
  114. subroutine-0.1.0/src/subroutine/domain/tasks.py +1169 -0
  115. subroutine-0.1.0/src/subroutine/domain/text.py +104 -0
  116. subroutine-0.1.0/src/subroutine/domain/tokens.py +138 -0
  117. subroutine-0.1.0/src/subroutine/domain/users.py +270 -0
  118. subroutine-0.1.0/src/subroutine/domain/versions.py +79 -0
  119. subroutine-0.1.0/src/subroutine/domain/workspaces.py +571 -0
  120. subroutine-0.1.0/src/subroutine/errors.py +590 -0
  121. subroutine-0.1.0/src/subroutine/fanout.py +198 -0
  122. subroutine-0.1.0/src/subroutine/mcp/__init__.py +15 -0
  123. subroutine-0.1.0/src/subroutine/mcp/protocol.py +283 -0
  124. subroutine-0.1.0/src/subroutine/mcp/session.py +78 -0
  125. subroutine-0.1.0/src/subroutine/mcp/tools.py +780 -0
  126. subroutine-0.1.0/src/subroutine/permissions.py +132 -0
  127. subroutine-0.1.0/src/subroutine/py.typed +0 -0
  128. subroutine-0.1.0/src/subroutine/views.py +1226 -0
  129. subroutine-0.1.0/tests/api_support.py +148 -0
  130. subroutine-0.1.0/tests/conftest.py +280 -0
  131. subroutine-0.1.0/tests/sample_models.py +64 -0
  132. subroutine-0.1.0/tests/test_actor_discipline.py +241 -0
  133. subroutine-0.1.0/tests/test_agenda.py +457 -0
  134. subroutine-0.1.0/tests/test_api_app.py +277 -0
  135. subroutine-0.1.0/tests/test_api_authentication.py +421 -0
  136. subroutine-0.1.0/tests/test_api_comments.py +233 -0
  137. subroutine-0.1.0/tests/test_api_concurrency.py +225 -0
  138. subroutine-0.1.0/tests/test_api_documents.py +374 -0
  139. subroutine-0.1.0/tests/test_api_events.py +336 -0
  140. subroutine-0.1.0/tests/test_api_examples.py +204 -0
  141. subroutine-0.1.0/tests/test_api_meta.py +375 -0
  142. subroutine-0.1.0/tests/test_api_problems.py +185 -0
  143. subroutine-0.1.0/tests/test_api_projects.py +338 -0
  144. subroutine-0.1.0/tests/test_api_routing.py +353 -0
  145. subroutine-0.1.0/tests/test_api_shaping.py +445 -0
  146. subroutine-0.1.0/tests/test_api_tasks.py +2103 -0
  147. subroutine-0.1.0/tests/test_api_tokens.py +261 -0
  148. subroutine-0.1.0/tests/test_api_workspaces.py +374 -0
  149. subroutine-0.1.0/tests/test_api_writability.py +499 -0
  150. subroutine-0.1.0/tests/test_auth.py +160 -0
  151. subroutine-0.1.0/tests/test_authentication.py +353 -0
  152. subroutine-0.1.0/tests/test_authorization.py +767 -0
  153. subroutine-0.1.0/tests/test_bootstrap.py +372 -0
  154. subroutine-0.1.0/tests/test_capture.py +616 -0
  155. subroutine-0.1.0/tests/test_cli_connections.py +965 -0
  156. subroutine-0.1.0/tests/test_cli_help.py +123 -0
  157. subroutine-0.1.0/tests/test_config.py +298 -0
  158. subroutine-0.1.0/tests/test_connections.py +486 -0
  159. subroutine-0.1.0/tests/test_context.py +229 -0
  160. subroutine-0.1.0/tests/test_dates.py +277 -0
  161. subroutine-0.1.0/tests/test_db_types.py +172 -0
  162. subroutine-0.1.0/tests/test_directory.py +190 -0
  163. subroutine-0.1.0/tests/test_documentation.py +91 -0
  164. subroutine-0.1.0/tests/test_durations.py +182 -0
  165. subroutine-0.1.0/tests/test_errors.py +204 -0
  166. subroutine-0.1.0/tests/test_fanout.py +230 -0
  167. subroutine-0.1.0/tests/test_fixtures.py +50 -0
  168. subroutine-0.1.0/tests/test_imports.py +428 -0
  169. subroutine-0.1.0/tests/test_instances.py +1165 -0
  170. subroutine-0.1.0/tests/test_isolation.py +251 -0
  171. subroutine-0.1.0/tests/test_local_mode.py +511 -0
  172. subroutine-0.1.0/tests/test_mcp.py +1039 -0
  173. subroutine-0.1.0/tests/test_migrations.py +713 -0
  174. subroutine-0.1.0/tests/test_models.py +510 -0
  175. subroutine-0.1.0/tests/test_multi_user.py +222 -0
  176. subroutine-0.1.0/tests/test_ordering.py +154 -0
  177. subroutine-0.1.0/tests/test_personal_path.py +2978 -0
  178. subroutine-0.1.0/tests/test_plugin.py +259 -0
  179. subroutine-0.1.0/tests/test_reach.py +563 -0
  180. subroutine-0.1.0/tests/test_release_notes.py +303 -0
  181. subroutine-0.1.0/tests/test_schedule.py +472 -0
  182. subroutine-0.1.0/tests/test_scoping.py +372 -0
  183. subroutine-0.1.0/tests/test_seed.py +436 -0
  184. subroutine-0.1.0/tests/test_services.py +1325 -0
  185. subroutine-0.1.0/tests/test_smoke.py +59 -0
  186. subroutine-0.1.0/tests/test_spec_endpoints.py +160 -0
  187. subroutine-0.1.0/tests/test_sqlite_concurrency.py +187 -0
  188. subroutine-0.1.0/tests/test_transfer.py +254 -0
  189. subroutine-0.1.0/tests/test_transport_equivalence.py +1373 -0
  190. subroutine-0.1.0/tests/test_upgrade.py +256 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "subroutine",
4
+ "description": "Project management for people and agents, in equal measure.",
5
+ "owner": {
6
+ "name": "Simon Holliday",
7
+ "email": "simon.holliday@protonmail.com"
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "subroutine",
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'.",
14
+ "author": {
15
+ "name": "Simon Holliday",
16
+ "email": "simon.holliday@protonmail.com"
17
+ },
18
+ "category": "productivity",
19
+ "keywords": [
20
+ "tasks",
21
+ "project-management",
22
+ "todo",
23
+ "self-hosted",
24
+ "planning"
25
+ ],
26
+ "homepage": "https://github.com/simonholliday/subroutine"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,221 @@
1
+ name: CI
2
+
3
+ # Two jobs rather than one matrix doing everything. Lint and type results are identical on
4
+ # every Python version, so running them three times would add nothing but three places to
5
+ # read the same failure.
6
+
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: ci-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ env:
21
+ PIP_DISABLE_PIP_VERSION_CHECK: "1"
22
+
23
+ jobs:
24
+ checks:
25
+ name: Lint and types
26
+ runs-on: ubuntu-latest
27
+
28
+ steps:
29
+ # Full history and tags, because the release check compares this commit's migration
30
+ # head against the one at the most recent tag. A shallow clone has neither, and the
31
+ # check would find no previous tag and cheerfully pass every time.
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ fetch-depth: 0
35
+
36
+ - uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.12"
39
+ cache: pip
40
+ cache-dependency-path: pyproject.toml
41
+
42
+ - name: Install
43
+ run: python -m pip install -e '.[dev,postgres]'
44
+
45
+ # `ruff check` only. `ruff format` would strip the space in `def foo (x)` and
46
+ # re-indent the whole codebase with spaces — it is not a formatter on this project.
47
+ - name: Ruff
48
+ run: ruff check .
49
+
50
+ - name: Mypy
51
+ run: mypy src tests scripts
52
+
53
+ # A release that moves the schema has to say so in CHANGELOG.md, so that somebody can
54
+ # *plan* the database upgrade rather than meet one halfway through installing
55
+ # something (SPEC.md §12.4a). Derived from the migration directory rather than from
56
+ # anybody remembering at release time, which is when nobody does.
57
+ - name: Release notes
58
+ run: python scripts/check_release_notes.py
59
+
60
+ # Subroutine is AGPL and is also offered commercially, and both rest on being able to
61
+ # grant rights in the whole of what ships. A copyleft dependency takes that away, and it
62
+ # will arrive inside somebody else's requirements rather than as a decision anybody
63
+ # makes. Separate job with a clean install: development tools are not distributed, so a
64
+ # GPL linter constrains nothing and should not fail this.
65
+ licences:
66
+ name: Dependency licences
67
+ runs-on: ubuntu-latest
68
+
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+
72
+ - uses: actions/setup-python@v5
73
+ with:
74
+ python-version: "3.12"
75
+ cache: pip
76
+ cache-dependency-path: pyproject.toml
77
+
78
+ # `packaging` is the script's own tool for walking the closure, not a dependency of
79
+ # Subroutine — it is installed here rather than declared, so it never ships.
80
+ - name: Install runtime dependencies only
81
+ run: python -m pip install '.[postgres]' packaging
82
+
83
+ - name: Check
84
+ run: python scripts/check_licences.py
85
+
86
+ # Checked by the tool that will read them (SPEC.md §21). `tests/test_plugin.py` covers what
87
+ # the manifests must *say* — that the version matches the package, that every declared option
88
+ # is substituted somewhere, that the token is held as a secret. Only Claude Code itself can
89
+ # say whether they conform to its schema, and a manifest that does not is one nobody can
90
+ # install at all.
91
+ #
92
+ # Its own job rather than a step in `checks`, because it needs a Node package the rest of the
93
+ # build has no use for. Jobs run in parallel, so this costs nothing on the clock and does not
94
+ # slow the lint feedback everybody actually waits for.
95
+ plugin:
96
+ name: Plugin manifests
97
+ runs-on: ubuntu-latest
98
+
99
+ steps:
100
+ - uses: actions/checkout@v4
101
+
102
+ - uses: actions/setup-node@v4
103
+ with:
104
+ node-version: "22"
105
+
106
+ - name: Install Claude Code
107
+ run: npm install -g @anthropic-ai/claude-code
108
+
109
+ - name: Validate
110
+ run: |
111
+ claude plugin validate .
112
+ claude plugin validate ./plugins/subroutine
113
+
114
+ test:
115
+ name: Tests (Python ${{ matrix.python-version }})
116
+ runs-on: ubuntu-latest
117
+
118
+ strategy:
119
+ fail-fast: false
120
+ matrix:
121
+ python-version: ["3.11", "3.12", "3.13"]
122
+
123
+ services:
124
+ postgres:
125
+ image: postgres:16
126
+ env:
127
+ POSTGRES_USER: postgres
128
+ POSTGRES_PASSWORD: postgres
129
+ POSTGRES_DB: postgres
130
+ ports:
131
+ - 5432:5432
132
+ # Without this the suite can start before the server accepts connections, and the
133
+ # PostgreSQL half would skip rather than fail — see SUBROUTINE_TEST_REQUIRE_POSTGRES.
134
+ options: >-
135
+ --health-cmd "pg_isready -U postgres"
136
+ --health-interval 5s
137
+ --health-timeout 5s
138
+ --health-retries 10
139
+
140
+ steps:
141
+ - uses: actions/checkout@v4
142
+
143
+ - uses: actions/setup-python@v5
144
+ with:
145
+ python-version: ${{ matrix.python-version }}
146
+ cache: pip
147
+ cache-dependency-path: pyproject.toml
148
+
149
+ - name: Install
150
+ run: python -m pip install -e '.[dev,postgres]'
151
+
152
+ # The whole suite runs twice, once per backend. The drift check that compares the
153
+ # models against the migration head is one of those tests, and it is the reason this
154
+ # job matters more than it looks: portability breaks are caught on the day they are
155
+ # introduced rather than at the first PostgreSQL deployment.
156
+ - name: Tests on SQLite and PostgreSQL
157
+ env:
158
+ SUBROUTINE_TEST_POSTGRES_ADMIN_URL: postgresql+psycopg://postgres:postgres@localhost:5432/postgres
159
+ SUBROUTINE_TEST_REQUIRE_POSTGRES: "1"
160
+ run: pytest -q
161
+
162
+ # Proves the thing a new user actually does, on a clean machine with nothing configured:
163
+ # install the package, run one command, get a working database. It is separate from the
164
+ # test job because it must not see the repository's test fixtures or a pre-made database.
165
+ first-run:
166
+ name: First run
167
+ runs-on: ubuntu-latest
168
+
169
+ steps:
170
+ - uses: actions/checkout@v4
171
+
172
+ - uses: actions/setup-python@v5
173
+ with:
174
+ python-version: "3.12"
175
+ cache: pip
176
+ cache-dependency-path: pyproject.toml
177
+
178
+ - name: Install as a user would
179
+ run: python -m pip install .
180
+
181
+ - name: subroutine init
182
+ run: |
183
+ set -euo pipefail
184
+ output="$(subroutine init 2>&1)"
185
+ echo "$output"
186
+ expected='Ready. Try: subroutine add "something to do"'
187
+ if [ "$output" != "$expected" ]; then
188
+ echo "init printed something other than its one line" >&2
189
+ exit 1
190
+ fi
191
+
192
+ - name: The database is queryable afterwards
193
+ run: |
194
+ set -euo pipefail
195
+ subroutine db current | grep -q '^Schema is at '
196
+ subroutine init | grep -q 'Already set up'
197
+
198
+ # SPEC.md §13.5b, the gating criterion for M2: a fresh installation to a working
199
+ # to-do list in three commands and a completed task in a fourth, with none of the
200
+ # output mentioning a workspace, a status, a project, a criterion, a verification, a
201
+ # session or a claim. The suite runs this too; it runs here as well because this job
202
+ # is the only one with nothing configured and no fixtures — which is the state the
203
+ # person it is written about is actually in.
204
+ - name: The personal test
205
+ run: |
206
+ set -euo pipefail
207
+ # `2>&1` on every one of them, because a warning is as visible to the person as
208
+ # an answer is. Capturing stdout alone is what let a checkout marker greet a
209
+ # stranger with the words *workspace* and *project* while this job reported
210
+ # success (`SR#219`, `SR#222`).
211
+ subroutine add "Call the dentist before Sunday" 2>&1 | tee /tmp/add.txt
212
+ subroutine today 2>&1 | tee /tmp/today.txt
213
+ subroutine done 1 2>&1 | tee /tmp/done.txt
214
+ grep -q 'Added: Call the dentist' /tmp/add.txt
215
+ grep -q 'Call the dentist' /tmp/today.txt
216
+ grep -q 'Done: Call the dentist' /tmp/done.txt
217
+ if grep -Eiq 'workspace|status|project|criterion|verification|session|claim' \
218
+ /tmp/add.txt /tmp/today.txt /tmp/done.txt; then
219
+ echo "The personal path mentioned something §1.4 says it must not." >&2
220
+ exit 1
221
+ fi
@@ -0,0 +1,178 @@
1
+ name: Release
2
+
3
+ # Publishing is the one act here that cannot be undone: PyPI refuses a version it has already
4
+ # seen, even after a deletion, so a broken 0.1.0 is not fixed but abandoned. Everything in this
5
+ # file exists to spend that single attempt well — the suite runs, the metadata is checked, and
6
+ # the tag is made to agree with the version.
7
+ #
8
+ # **A failed upload spends nothing; only a successful upload of a bad artefact does.** That is
9
+ # why TestPyPI is a rehearsal somebody asks for rather than a gate on the way through. What it
10
+ # proves is the OIDC handshake, and if that is misconfigured the real upload simply fails and is
11
+ # retried; what it does not prove is the artefact, which `twine check` and a clean install from
12
+ # the built wheel already cover. Registering a publisher there is a separate account and a
13
+ # separate act, so a release must not wait on one.
14
+ #
15
+ # Hence: a **tag** publishes to PyPI, a **manual** run rehearses on TestPyPI, and neither can do
16
+ # the other's job by accident.
17
+ #
18
+ # No stored credential anywhere. Both uploads authenticate by OIDC against a trusted publisher
19
+ # (SPEC.md §2.2), which is why each publishing job asks for `id-token: write` and nothing else
20
+ # does.
21
+
22
+ on:
23
+ push:
24
+ tags: ["v*"]
25
+ workflow_dispatch:
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ # CI runs on pushes to main, and a tag is not one — so without this a release could publish
32
+ # code no suite ever saw. One version rather than the full matrix: this is a gate on the
33
+ # artefact, not the compatibility run that already happened on the commit.
34
+ test:
35
+ name: Tests
36
+ runs-on: ubuntu-latest
37
+
38
+ services:
39
+ postgres:
40
+ image: postgres:16
41
+ env:
42
+ POSTGRES_USER: postgres
43
+ POSTGRES_PASSWORD: postgres
44
+ POSTGRES_DB: postgres
45
+ ports:
46
+ - 5432:5432
47
+ options: >-
48
+ --health-cmd "pg_isready -U postgres"
49
+ --health-interval 5s
50
+ --health-timeout 5s
51
+ --health-retries 10
52
+
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+
56
+ - uses: actions/setup-python@v5
57
+ with:
58
+ python-version: "3.12"
59
+ cache: pip
60
+ cache-dependency-path: pyproject.toml
61
+
62
+ - name: Install
63
+ run: python -m pip install -e '.[dev,postgres]'
64
+
65
+ # Both backends, and an unreachable PostgreSQL is a failure rather than a skip — a
66
+ # release verified on half a test run is not verified.
67
+ - name: Tests on SQLite and PostgreSQL
68
+ env:
69
+ SUBROUTINE_TEST_POSTGRES_ADMIN_URL: postgresql+psycopg://postgres:postgres@localhost:5432/postgres
70
+ SUBROUTINE_TEST_REQUIRE_POSTGRES: "1"
71
+ run: pytest
72
+
73
+ build:
74
+ name: Build
75
+ needs: test
76
+ runs-on: ubuntu-latest
77
+
78
+ steps:
79
+ # Full history and tags: `check_release_notes.py` compares this commit's migration head
80
+ # against the head at the previous tag, and a shallow clone has neither.
81
+ - uses: actions/checkout@v4
82
+ with:
83
+ fetch-depth: 0
84
+
85
+ - uses: actions/setup-python@v5
86
+ with:
87
+ python-version: "3.12"
88
+
89
+ # Subroutine itself as well as the build tools, because `check_release_notes.py` reads
90
+ # the migration head through `subroutine.db.migrate` rather than by parsing files — the
91
+ # asymmetry that script's own docstring argues for. Installing only `build` and `twine`
92
+ # left it with `ModuleNotFoundError: No module named 'subroutine'`, which passed every
93
+ # local check because a shell that can run the script already has the package (`SR#230`).
94
+ - name: Install the build tools, and Subroutine itself
95
+ run: python -m pip install -e . build twine
96
+
97
+ - name: Build the wheel and the sdist
98
+ run: python -m build
99
+
100
+ - name: Check the metadata
101
+ run: twine check dist/*
102
+
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
107
+ if: startsWith(github.ref, 'refs/tags/')
108
+ run: |
109
+ set -euo pipefail
110
+ tagged="${GITHUB_REF_NAME#v}"
111
+ built="$(python -c 'import pathlib, tomllib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
112
+ if [ "$tagged" != "$built" ]; then
113
+ echo "the tag $GITHUB_REF_NAME does not name version $built" >&2
114
+ exit 1
115
+ fi
116
+ echo "tag and version agree on $built"
117
+
118
+ # A release that moves the schema has to say so in the changelog (SPEC.md §12.4a). CI
119
+ # checks this on every commit; it is checked again here because this is the run where
120
+ # being wrong reaches somebody else's database.
121
+ - name: Release notes
122
+ run: python scripts/check_release_notes.py
123
+
124
+ - uses: actions/upload-artifact@v4
125
+ with:
126
+ name: dist
127
+ path: dist/
128
+
129
+ # The rehearsal: same artefacts, same mechanism, a registry where a mistake costs nothing —
130
+ # run when somebody asks for it. Skipped entirely on a tag, so a release is never held up for
131
+ # the want of a publisher registered on a different site.
132
+ testpypi:
133
+ name: TestPyPI
134
+ needs: build
135
+ if: github.event_name == 'workflow_dispatch'
136
+ runs-on: ubuntu-latest
137
+
138
+ environment:
139
+ name: testpypi
140
+ url: https://test.pypi.org/p/subroutine
141
+
142
+ permissions:
143
+ id-token: write
144
+
145
+ steps:
146
+ - uses: actions/download-artifact@v4
147
+ with:
148
+ name: dist
149
+ path: dist/
150
+
151
+ - uses: pypa/gh-action-pypi-publish@release/v1
152
+ with:
153
+ repository-url: https://test.pypi.org/legacy/
154
+
155
+ # `push` can only mean a tag here, since that is this workflow's only push trigger — so the
156
+ # condition says "a release, not a rehearsal" rather than restating the trigger. Guarded by
157
+ # the `pypi` environment as well, so a required reviewer on it makes the upload a decision
158
+ # somebody takes rather than a consequence of pushing a tag.
159
+ pypi:
160
+ name: PyPI
161
+ needs: build
162
+ if: github.event_name == 'push'
163
+ runs-on: ubuntu-latest
164
+
165
+ environment:
166
+ name: pypi
167
+ url: https://pypi.org/p/subroutine
168
+
169
+ permissions:
170
+ id-token: write
171
+
172
+ steps:
173
+ - uses: actions/download-artifact@v4
174
+ with:
175
+ name: dist
176
+ path: dist/
177
+
178
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,53 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ .Python
6
+ build/
7
+ dist/
8
+ *.egg-info/
9
+ .eggs/
10
+
11
+ # Virtualenvs — the house convention keeps these outside the tree, but guard anyway
12
+ .venv/
13
+ venv/
14
+ env/
15
+
16
+ # Tooling caches
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ .coverage
21
+ .coverage.*
22
+ htmlcov/
23
+ coverage.xml
24
+
25
+ # Local data — a stray `subroutine init` in the working tree must never be committed
26
+ *.db
27
+ *.db-wal
28
+ *.db-shm
29
+ *.sqlite3
30
+ /data/
31
+ /instance/
32
+
33
+ # Secrets and local configuration
34
+ .env
35
+ .env.*
36
+ config.local.toml
37
+
38
+ # Editors
39
+ .idea/
40
+ *.swp
41
+ .DS_Store
42
+
43
+ # Local documentation
44
+ CLAUDE.md
45
+ COMPETITIVE-ANALYSIS.md
46
+ SPEC.md
47
+ MVP-PLAN.md
48
+ reviews
49
+
50
+ # Committing this is right for a team sharing one instance, and wrong here: a clone of
51
+ # *Subroutine itself* goes to strangers who share nothing, and the marker would name a
52
+ # workspace and a project that exist on no machine but ours (`SR#219`).
53
+ .subroutine
@@ -0,0 +1,76 @@
1
+ # Changelog
2
+
3
+ Notable changes, newest first. Dates are the date of the release.
4
+
5
+ **A release that changes the database schema says so at the top of its own section**, and CI
6
+ fails if it does not. `scripts/check_release_notes.py` compares the migration head against the
7
+ most recent tag rather than trusting anybody to remember, so the notice appears in the
8
+ unreleased section on the day the migration lands — written by whoever wrote the migration, not
9
+ by whoever cuts the release in a hurry three weeks later. `--emit` prints the wording.
10
+
11
+ The point of it is that you can *plan* a database upgrade instead of meeting one halfway
12
+ through installing something. See [docs/hosting.md](docs/hosting.md#upgrading) for what the
13
+ upgrade involves.
14
+
15
+ ## 0.1.0 — 2026-08-01
16
+
17
+ The first public release. Everything here is new, so this section says what exists rather than
18
+ what changed.
19
+
20
+ No migration notice: there is no previous release to upgrade from, and a fresh install builds
21
+ its schema outright with `subroutine init`.
22
+
23
+ ### A personal to-do list
24
+
25
+ - `subroutine init`, `add`, `today`, `done`, `plan`, `defer`, `list`, `search`, `show`,
26
+ `comment`, `project create`, `project list` and `doc create`. Three commands from install to
27
+ a working list, and a fourth to tick something off.
28
+ - Quick capture: `subroutine add "Fix the deploy script by friday !4/2 ~2h #ops"` sets the
29
+ deadline, both priority axes, the estimate and a tag from the line you already type.
30
+ - Items are addressed by a number allocated once and never reused, so it goes on meaning that
31
+ item after you have finished a dozen others.
32
+ - `subroutine start` and `subroutine stop`, so the list can say what you are in the middle of.
33
+ - `subroutine link 42 blocks 43` and `subroutine unlink`. `--ready` reads those links, so
34
+ this is how that filter learns anything.
35
+ - `subroutine delete` and `subroutine restore`, with `list --trash` to see what is in there.
36
+ Deleting is soft, so the wrong number costs nothing.
37
+ - `subroutine list --ready` shows only work that can actually be started — nothing unfinished
38
+ blocks it and it is not deferred. It is the question a backlog cannot answer.
39
+ - `subroutine help` and `subroutine --help` do the same thing: they list the commands.
40
+ `subroutine explain` covers the ideas behind them — dates, refs, the capture shorthand.
41
+
42
+ ### An HTTP API, and agents as first-class users
43
+
44
+ - The HTTP API under `/v1`, with the same data model and the same permission checks the
45
+ CLI uses. `GET /v1/docs/agent` is the guide an agent should read first.
46
+ - Scoped bearer tokens: an agent's credential can be narrower than the person who issued it,
47
+ and may never be wider. Per-workspace pins and per-permission scopes.
48
+ - `subroutine token create --username ana` issues for a person, `--service-account claude` for
49
+ a machine identity, creating it as it goes. Two flags because they are two decisions, and
50
+ neither will issue a credential for an account that could not use it.
51
+ - `subroutine mcp` serves the same instance over the Model Context Protocol, in nine tools —
52
+ including `link` and `project`, so an agent can say what blocks what and file its own work.
53
+ - **A Claude Code plugin**, which wires those tools up and carries a skill describing the
54
+ practice — including how to adopt Subroutine in a project that does not use it yet.
55
+ - Attribution on everything, a comment thread per item, and a history of every change.
56
+
57
+ ### Running it
58
+
59
+ - `subroutine serve` listens on loopback and refuses a wider bind without TLS in front of it.
60
+ - SQLite by default with no configuration; PostgreSQL with the `postgres` extra.
61
+ - `subroutine upgrade` — reports both schema versions, takes a verified backup, migrates, then
62
+ reads the result back. It does not install software, deliberately.
63
+ - Backups to a directory of your choosing, verified where they land, and a restore that makes
64
+ you say whether it is a recovery or a clone.
65
+ - Separate instances on one machine with `--profile`, isolated across all three XDG roots.
66
+
67
+ ### Known limits
68
+
69
+ Session handoffs, verification evidence and claims are specified and not built — as are
70
+ attachments, calendar feeds, recurring tasks, a `GET /v1/changes` feed, and manual reordering.
71
+ There is no web UI.
72
+
73
+ Two of those are *refused out loud* rather than merely absent, which is worth telling apart:
74
+ the capture grammar recognises `every monday` well enough to leave it in your title and say it
75
+ did nothing with it, and a calendar credential presented to the API is turned down by name.
76
+ The rest are simply not there yet.
@@ -0,0 +1,78 @@
1
+ # Subroutine Contributor Licence Agreement
2
+
3
+ Adapted from the Apache Software Foundation Individual Contributor Licence Agreement
4
+ v2.0. Version 1.0, dated 2026-07-29.
5
+
6
+ You agree to this by saying so in your pull request:
7
+
8
+ > I have read the CLA document and I hereby agree to its terms.
9
+
10
+ ## Why this exists
11
+
12
+ Subroutine is published under AGPL-3.0-or-later and is also offered under a commercial
13
+ licence by separate agreement. Offering both requires the ability to grant rights in the
14
+ whole of the code, including your contribution. This document is how you grant those
15
+ rights.
16
+
17
+ **You keep your copyright.** This is a licence, not an assignment. Your contribution
18
+ remains yours and you may use it however you wish, elsewhere and for any purpose.
19
+
20
+ ## Agreement
21
+
22
+ By contributing to Subroutine you accept the following terms for your present and future
23
+ contributions. If you do not agree, please do not contribute.
24
+
25
+ **1. Definitions.** "You" means the copyright owner, or the person legally authorised by
26
+ the copyright owner, entering into this agreement. "The Project Owner" means Simon
27
+ Holliday. "Contribution" means any original work of authorship, including any changes or
28
+ additions to existing work, that You intentionally submit to the Project Owner for
29
+ inclusion in Subroutine. "Submit" means any form of communication sent to the Project
30
+ Owner or its representatives, including but not limited to source control systems, issue
31
+ trackers and mailing lists, but excluding communication conspicuously marked "Not a
32
+ Contribution".
33
+
34
+ **2. Grant of copyright licence.** You grant to the Project Owner, and to recipients of
35
+ software distributed by the Project Owner, a perpetual, worldwide, non-exclusive,
36
+ royalty-free, irrevocable copyright licence to reproduce, prepare derivative works of,
37
+ publicly display, publicly perform, sublicense and distribute Your Contributions and
38
+ such derivative works.
39
+
40
+ This licence expressly includes the right to sublicense and distribute Your Contributions
41
+ **under any licence terms, including proprietary terms**, whether or not those terms are
42
+ the ones under which Subroutine is currently published. This is the clause that makes
43
+ commercial licensing of Subroutine possible, and it is the reason this document exists.
44
+
45
+ **3. Grant of patent licence.** You grant to the Project Owner, and to recipients of
46
+ software distributed by the Project Owner, a perpetual, worldwide, non-exclusive,
47
+ royalty-free, irrevocable patent licence to make, have made, use, offer to sell, sell,
48
+ import and otherwise transfer Subroutine. This applies only to those patent claims
49
+ licensable by You that are necessarily infringed by Your Contribution alone or by its
50
+ combination with Subroutine.
51
+
52
+ If any entity brings patent litigation alleging that Subroutine, or a Contribution
53
+ incorporated within it, constitutes direct or contributory patent infringement, any
54
+ patent licences granted to that entity under this agreement terminate as of the date the
55
+ litigation is filed.
56
+
57
+ **4. You have the right to grant this.** You represent that You are legally entitled to
58
+ grant the above licences, and that each of Your Contributions is Your original creation.
59
+ If Your employer has rights to intellectual property You create, You represent that You
60
+ have received permission to make the Contributions on behalf of that employer, that Your
61
+ employer has waived such rights, or that Your employer has executed a separate agreement
62
+ with the Project Owner.
63
+
64
+ **5. Work that is not Yours.** You may submit work that is not Your original creation
65
+ only if it is clearly identified as such, complete with its source and any licence or
66
+ other restriction attached to it. Mark it conspicuously as "Submitted on behalf of a
67
+ third party: [named here]".
68
+
69
+ **6. No warranty and no obligation.** You are not expected to provide support for Your
70
+ Contributions. Except for the representations in section 4, and unless required by
71
+ applicable law or agreed in writing, You provide Your Contributions on an "AS IS" basis,
72
+ without warranties or conditions of any kind, either express or implied, including
73
+ without limitation any warranty of title, non-infringement, merchantability or fitness
74
+ for a particular purpose.
75
+
76
+ **7. Tell us if something changes.** You agree to notify the Project Owner if You become
77
+ aware of any fact or circumstance that would make any of these representations inaccurate
78
+ in any respect.