taskops-cli 0.5.1__tar.gz → 0.5.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 (297) hide show
  1. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ARCHITECTURE.md +286 -2
  2. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/CHANGELOG.md +51 -0
  3. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/CLAUDE.md +57 -7
  4. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/PKG-INFO +80 -9
  5. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/README.md +79 -8
  6. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/smoke.sh +91 -10
  7. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_version.py +1 -1
  8. taskops_cli-0.5.2/src/taskops/cli/gitremote.py +198 -0
  9. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/main.py +10 -1
  10. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/operate.py +2 -2
  11. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/parser.py +18 -3
  12. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/remote.py +11 -1
  13. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/forge.py +10 -0
  14. taskops_cli-0.5.2/src/taskops/gitwork/bare.py +122 -0
  15. taskops_cli-0.5.2/src/taskops/gitwork/onward.py +168 -0
  16. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/run.py +83 -1
  17. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/auth.py +22 -0
  18. taskops_cli-0.5.2/src/taskops/http/gitbody.py +69 -0
  19. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/gitdoor.py +30 -28
  20. taskops_cli-0.5.2/src/taskops/http/gitpack.py +176 -0
  21. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/handler.py +16 -33
  22. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/mounts.py +3 -3
  23. taskops_cli-0.5.2/src/taskops/http/page.py +42 -0
  24. taskops_cli-0.5.2/src/taskops/http/repos.py +109 -0
  25. taskops_cli-0.5.2/src/taskops/http/stale.py +95 -0
  26. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/static.py +24 -19
  27. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/boardview.py +20 -0
  28. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/live.py +5 -0
  29. taskops_cli-0.5.2/src/taskops/store/mirroring.py +96 -0
  30. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/ui/app.js +9 -9
  31. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/pulse.py +5 -0
  32. taskops_cli-0.5.2/tests/test_mirror.py +264 -0
  33. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_topology.py +662 -120
  34. taskops_cli-0.5.2/ui/smoke/sections/card-head-sha.tsx +84 -0
  35. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/Sections.tsx +8 -4
  36. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/links.tsx +38 -0
  37. taskops_cli-0.5.1/src/taskops/gitwork/mirror.py +0 -132
  38. taskops_cli-0.5.1/src/taskops/http/repos.py +0 -98
  39. taskops_cli-0.5.1/src/taskops/http/stale.py +0 -62
  40. taskops_cli-0.5.1/tests/test_mirror.py +0 -164
  41. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.claude/settings.json +0 -0
  42. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.gitattributes +0 -0
  43. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.github/workflows/ci.yml +0 -0
  44. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.github/workflows/workflow.yml +0 -0
  45. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.gitignore +0 -0
  46. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.mcp.json +0 -0
  47. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/cache.sqlite +0 -0
  48. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/cache.sqlite-shm +0 -0
  49. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/cache.sqlite-wal +0 -0
  50. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/events.jsonl +0 -0
  51. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/live.sqlite +0 -0
  52. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/live.sqlite-shm +0 -0
  53. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board/live.sqlite-wal +0 -0
  54. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/board.json +0 -0
  55. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/reports/lifecycle-chapter.html +0 -0
  56. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/reports/reports-chapter.html +0 -0
  57. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/.taskops/ui.lock +0 -0
  58. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/pyproject.toml +0 -0
  59. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/__init__.py +0 -0
  60. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_clock.py +0 -0
  61. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_errors.py +0 -0
  62. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_ids.py +0 -0
  63. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_json.py +0 -0
  64. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_locate.py +0 -0
  65. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/_wire.py +0 -0
  66. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/board.py +0 -0
  67. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/__init__.py +0 -0
  68. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/__main__.py +0 -0
  69. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/admin.py +0 -0
  70. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/claude.py +0 -0
  71. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/commands.py +0 -0
  72. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/enrol.py +0 -0
  73. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/github.py +0 -0
  74. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/grants.py +0 -0
  75. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/hooks.py +0 -0
  76. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/paging.py +0 -0
  77. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/pull.py +0 -0
  78. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/push.py +0 -0
  79. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/rm.py +0 -0
  80. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/serving.py +0 -0
  81. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/team.py +0 -0
  82. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/watch.py +0 -0
  83. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/window.py +0 -0
  84. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/cli/wording.py +0 -0
  85. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/__init__.py +0 -0
  86. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/actors.py +0 -0
  87. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/challenge.py +0 -0
  88. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/chapters.py +0 -0
  89. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/event.py +0 -0
  90. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/graph.py +0 -0
  91. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/holding.py +0 -0
  92. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/hours.py +0 -0
  93. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/kinds.py +0 -0
  94. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/machine.py +0 -0
  95. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/mentions.py +0 -0
  96. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/replay.py +0 -0
  97. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/reports.py +0 -0
  98. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/review.py +0 -0
  99. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/scope.py +0 -0
  100. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/seams.py +0 -0
  101. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/core/types.py +0 -0
  102. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/__init__.py +0 -0
  103. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/bind.py +0 -0
  104. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/catchup.py +0 -0
  105. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/claudefiles.py +0 -0
  106. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/diff.py +0 -0
  107. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/install.py +0 -0
  108. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/landing.py +0 -0
  109. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/patch.py +0 -0
  110. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/remote.py +0 -0
  111. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/sig.py +0 -0
  112. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/trailer.py +0 -0
  113. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/gitwork/trees.py +0 -0
  114. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/__init__.py +0 -0
  115. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/admin.py +0 -0
  116. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/feed.py +0 -0
  117. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/grants.py +0 -0
  118. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/ingest.py +0 -0
  119. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/login.py +0 -0
  120. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/members.py +0 -0
  121. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/removal.py +0 -0
  122. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/routes.py +0 -0
  123. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/rpc.py +0 -0
  124. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/scoped.py +0 -0
  125. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/server.py +0 -0
  126. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/upstream.py +0 -0
  127. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/http/watcher.py +0 -0
  128. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/identity.py +0 -0
  129. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/__init__.py +0 -0
  130. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/__main__.py +0 -0
  131. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/activity.py +0 -0
  132. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/before.py +0 -0
  133. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/boards.py +0 -0
  134. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/brief.py +0 -0
  135. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/chapter.py +0 -0
  136. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/dossier.py +0 -0
  137. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/fields.py +0 -0
  138. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/gitmoves.py +0 -0
  139. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/hello.py +0 -0
  140. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/integrate.py +0 -0
  141. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/orders.py +0 -0
  142. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/render.py +0 -0
  143. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/schema.py +0 -0
  144. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/server.py +0 -0
  145. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/thread.py +0 -0
  146. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/mcp/tools.py +0 -0
  147. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/py.typed +0 -0
  148. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/session.py +0 -0
  149. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/__init__.py +0 -0
  150. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/cache.py +0 -0
  151. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/creds.py +0 -0
  152. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/handover.py +0 -0
  153. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/log.py +0 -0
  154. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/pubkeys.py +0 -0
  155. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/reviews.py +0 -0
  156. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/server.py +0 -0
  157. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/store/stores.py +0 -0
  158. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/ui/index.html +0 -0
  159. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/ui/style.css +0 -0
  160. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/__init__.py +0 -0
  161. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_args.py +0 -0
  162. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_cards.py +0 -0
  163. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_chapter.py +0 -0
  164. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_context.py +0 -0
  165. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_facts.py +0 -0
  166. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_mentions.py +0 -0
  167. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_rows.py +0 -0
  168. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_stories.py +0 -0
  169. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_waiting.py +0 -0
  170. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/_windows.py +0 -0
  171. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/activity.py +0 -0
  172. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/assign.py +0 -0
  173. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/card.py +0 -0
  174. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/events.py +0 -0
  175. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/filed.py +0 -0
  176. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/plan.py +0 -0
  177. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/project.py +0 -0
  178. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/record.py +0 -0
  179. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/report.py +0 -0
  180. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/review.py +0 -0
  181. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/take.py +0 -0
  182. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/src/taskops/verbs/update.py +0 -0
  183. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/__init__.py +0 -0
  184. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/conftest.py +0 -0
  185. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_architecture.py +0 -0
  186. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_claude.py +0 -0
  187. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_core.py +0 -0
  188. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_git.py +0 -0
  189. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_mcp.py +0 -0
  190. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_report.py +0 -0
  191. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_store.py +0 -0
  192. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_ui.py +0 -0
  193. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_verbs.py +0 -0
  194. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/test_window.py +0 -0
  195. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/tests/ui_harness.js +0 -0
  196. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/build.mjs +0 -0
  197. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/index.html +0 -0
  198. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/package-lock.json +0 -0
  199. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/package.json +0 -0
  200. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/fixture.json +0 -0
  201. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/main.tsx +0 -0
  202. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/run.mjs +0 -0
  203. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/actors-window-filter.tsx +0 -0
  204. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/actors.tsx +0 -0
  205. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/all-chapters-scope.tsx +0 -0
  206. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/board-columns.tsx +0 -0
  207. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/board-flow-view.tsx +0 -0
  208. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
  209. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/board-root-mount.tsx +0 -0
  210. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
  211. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/chapter-landed.tsx +0 -0
  212. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
  213. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/chapters-open.tsx +0 -0
  214. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/comment-box.tsx +0 -0
  215. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
  216. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
  217. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/diff-page.tsx +0 -0
  218. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/dossier.tsx +0 -0
  219. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/event-stream.tsx +0 -0
  220. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/feed-reconnect.tsx +0 -0
  221. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/forge-links.tsx +0 -0
  222. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
  223. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/git-diff.tsx +0 -0
  224. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/hosted-window.tsx +0 -0
  225. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/monitor-panes.tsx +0 -0
  226. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/overlay-escape.tsx +0 -0
  227. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/report-markdown.tsx +0 -0
  228. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/report-sandbox.tsx +0 -0
  229. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/reviewing-skew.tsx +0 -0
  230. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/section.ts +0 -0
  231. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/swarm-topology.tsx +0 -0
  232. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/thread-closing-note.tsx +0 -0
  233. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections/worktrees-index.tsx +0 -0
  234. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/smoke/sections.mjs +0 -0
  235. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/App.tsx +0 -0
  236. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/client.ts +0 -0
  237. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/completed.ts +0 -0
  238. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/actors/Daysheet.tsx +0 -0
  239. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/actors/DevPanel.tsx +0 -0
  240. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/board/CardTile.tsx +0 -0
  241. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/board/Column.tsx +0 -0
  242. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/board/ViewToggle.tsx +0 -0
  243. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/board/flip.ts +0 -0
  244. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/board/useFlip.ts +0 -0
  245. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/CommentBox.tsx +0 -0
  246. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/Drawer.tsx +0 -0
  247. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/Patch.tsx +0 -0
  248. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/Thread.tsx +0 -0
  249. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/split.ts +0 -0
  250. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/card/tokens.ts +0 -0
  251. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/chrome/AvatarStack.tsx +0 -0
  252. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/chrome/Header.tsx +0 -0
  253. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/chrome/KpiRail.tsx +0 -0
  254. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
  255. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/chrome/TabNav.tsx +0 -0
  256. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/flow/FlowView.tsx +0 -0
  257. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/flow/layout.ts +0 -0
  258. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/Chapter.tsx +0 -0
  259. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/DependencyChain.tsx +0 -0
  260. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/EditSurface.tsx +0 -0
  261. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/EventStream.tsx +0 -0
  262. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
  263. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/LiveLeases.tsx +0 -0
  264. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/Mentions.tsx +0 -0
  265. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/Pane.tsx +0 -0
  266. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/Swarm.tsx +0 -0
  267. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/Throughput.tsx +0 -0
  268. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/monitor/panels.ts +0 -0
  269. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/reports/ReportFrame.tsx +0 -0
  270. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/shared/Avatar.tsx +0 -0
  271. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/shared/Markdown.tsx +0 -0
  272. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/shared/Overlay.tsx +0 -0
  273. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/shared/Segmented.tsx +0 -0
  274. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/shared/overlayStack.ts +0 -0
  275. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/story/ChapterStory.tsx +0 -0
  276. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/story/stats.ts +0 -0
  277. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/toasts/ToastStack.tsx +0 -0
  278. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/toasts/model.ts +0 -0
  279. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/components/toasts/useToasts.ts +0 -0
  280. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/format.ts +0 -0
  281. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/hoursWindow.ts +0 -0
  282. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/main.tsx +0 -0
  283. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/markdown.ts +0 -0
  284. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/pages/Actors.tsx +0 -0
  285. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/pages/Board.tsx +0 -0
  286. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/pages/Monitor.tsx +0 -0
  287. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/pages/Reports.tsx +0 -0
  288. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/pages/WorktreeDiff.tsx +0 -0
  289. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/pages/Worktrees.tsx +0 -0
  290. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/theme/theme.ts +0 -0
  291. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/theme/tokens.css +0 -0
  292. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/types.ts +0 -0
  293. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/useBoard.ts +0 -0
  294. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/src/useEvents.ts +0 -0
  295. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/styles/index.css +0 -0
  296. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/ui/tsconfig.json +0 -0
  297. {taskops_cli-0.5.1 → taskops_cli-0.5.2}/uv.lock +0 -0
@@ -837,14 +837,15 @@ nobody has touched yet.
837
837
  | a `recover` verb | doing is derived from the live lease; nothing is ever wrong to recover. Handing a card on is not that verb and never grew into one — it stayed inside `assign`, needs a named replacement, and is argued in §12 | no entry in `verbs/__init__.py::REGISTRY`; `tests/test_verbs.py::test_a_dead_workers_card_comes_back_by_itself` |
838
838
  | a reviewer ROLE, a stored review STATUS, automatic reviewer assignment | v1's review system: `peer` deadlocks, 14 closing rules over 6 modules, reviewers eating the budget of the work | review EXISTS since 2026-08-07 but narrowed: optional per card, derived from history-only events + a second lease, judged by an ordinary agent that may never judge its own work. `CARD_STATUSES` stays three; there is no reviewer role and nothing auto-assigns |
839
839
  | AUTOMATIC merges to the trunk | v1's `land` merged as a side effect of closing a card and ran checkout under working agents | a CARD cannot be merged to the trunk — `taskops_merge task=` takes no target. A finished MILESTONE lands via `taskops_merge milestone=` (2026-08-07): explicit, refused while any card is open or unintegrated, refused off-trunk, recorded as a `milestone landed` event. What stays impossible is the trunk moving as a side effect of anything |
840
- | git replication between clones | split-brain, two machines "owning" the same card | `RemoteBoard` never falls back to a local store on write failure (`Unreachable` instead) |
840
+ | git replication between clones | split-brain, two machines "owning" the same card | `RemoteBoard` never falls back to a local store on write failure (`Unreachable` instead). Narrowed on 2026-08-31, not lifted: the host became the board's git REMOTE (§16, "The host becomes the remote") — a hub, one direction on each leg (worktree → host, host → forge). What stays banned is BETWEEN clones: no clone pushes to another clone, and the host never pulls git from the forge |
841
841
  | Claude hooks **that decide or store** | latency, another thing to install and drift; v1's held state and gated actions | context travels in `initialize.instructions` + tool responses. The ONE exception, sanctioned 2026-08-06: `taskops hook claude`, delivery-only — it reads, injects a ✉ or ◆ line, and can be deleted with no loss but immediacy. `tests/test_claude.py` pins its safety properties, one test each — the module docstring lists them and `grep -c '^def test_' tests/test_claude.py` counts them |
842
842
  | a mark-as-read / ack verb for mentions | a stored `read` flag is `recover` again: a write whose only job is to contradict an earlier one | `core/mentions.py::pending()` derives it from the thread; `tests/test_verbs.py::test_a_mention_clears_itself_the_moment_the_actor_touches_the_card` |
843
843
  | PER-REQUEST SIGNING (every call carrying an SSHSIG envelope, no sessions) | it was the first design and sessions won on cost, not on taste: a signature per request means teaching a signature envelope to THREE transports — the stdlib `http.server`, the WebSocket handshake on `/feed`, and the MCP layer that opens a board once and holds it — and every one of them would have had to learn nonce replay, clock skew and canonicalisation separately, for exactly the result a bearer already has. It also needs the private key FILE on every call, in every worker, forever; a session needs it twice a day. And the fleet argument decides it: a signed envelope is a NEW wire format, so production's four boards would have had to be re-joined, which rule 3 forbids | `/login` is the only door that verifies a signature (`http/login.py`), and what it hands back is an ordinary `Credential` — the same row, the same table, the same `Authorization: Bearer` every legacy token uses (`store/creds.py`). The client half is `session.py` (the token's life: mint, refresh, remember) with `identity.py` beside it (WHO signs in and with WHICH key — `discover_key`, `establish`, the one door `--key` comes in through); each is well under the budget *because* each has one job. `tests/test_topology.py::test_a_legacy_only_board_still_works_on_rpc` and its three siblings are the fleet half: production's exact state — no principal, no key, an empty `allowed_signers` — driven through /rpc, /feed, the MCP handshake and the `taskops ui` window |
844
844
  | HAND-ROLLED CRYPTO, and a pip crypto DEPENDENCY | two ways to lose the same argument. Writing ed25519 verification by hand is the classic own-goal; adding `cryptography` or `PyNaCl` to buy it back breaks "a wheel and a directory" — the property that makes `pip install taskops` on a bare box a deploy (§17) — and puts a compiled wheel in the path of every agent's install | the verifier is OpenSSH's own: `ssh-keygen -Y sign` / `-Y verify -f allowed_signers`, the same SSHSIG mechanism git uses to sign commits, invoked through the ONE subprocess module (`gitwork/sig.py`, under `gitwork/run.py`). `pyproject.toml` has no runtime dependency at all, and `tests/test_architecture.py` keeps `subprocess` out of every layer but that one |
845
- | ANONYMOUS WRITES, in any form — including the invisible one | a public board is anonymous READ and nothing else. The subtle failure is not a card somebody could see: every read verb opens with `stores.live.renew(actor, now)`, an INSERT into `presence`, so a public board without a guard has every visitor writing to `live.sqlite` on every page load — no event, no card, nothing any ordinary test would notice. There is also no "anonymous-write grace" and no third visibility | `http/auth.py::anonymous` never hands out a credential with more than `{"read"}`, and refuses a write with the sentence that names how a key gets registered; `store/live.py::renew` is the ONE place that decides the presence row. `tests/test_topology.py::test_an_anonymous_crawl_of_a_public_board_moves_not_one_byte` asserts `events.jsonl` and `live.sqlite` (with `-wal` and `-shm`) hash-identical across a crawl of every read door there is, and `…::test_anonymous_may_not_claim_to_be_somebody` closes the `actor=` hole |
845
+ | ANONYMOUS WRITES, in any form — including the invisible one | a public board is anonymous READ and nothing else. The subtle failure is not a card somebody could see: every read verb opens with `stores.live.renew(actor, now)`, an INSERT into `presence`, so a public board without a guard has every visitor writing to `live.sqlite` on every page load — no event, no card, nothing any ordinary test would notice. There is also no "anonymous-write grace" and no third visibility. A `git push` to the host's `repo.git` is a write under this ban, exactly as an event is (§16, "The host becomes the remote") | `http/auth.py::anonymous` never hands out a credential with more than `{"read"}`, and refuses a write with the sentence that names how a key gets registered; `store/live.py::renew` is the ONE place that decides the presence row. `tests/test_topology.py::test_an_anonymous_crawl_of_a_public_board_moves_not_one_byte` asserts `events.jsonl` and `live.sqlite` (with `-wal` and `-shm`) hash-identical across a crawl of every read door there is, and `…::test_anonymous_may_not_claim_to_be_somebody` closes the `actor=` hole |
846
846
  | a `--force` on `taskops board push`, and a SYNC channel behind `board.ingest` | a non-empty target means two histories, and giving them an order they never had fabricates a timeline the board never observed. The precondition ("no history but its own beginning") is true exactly once in a board's life, which is what keeps the door a promotion and not replication between clones — the thing banned two rows up | `http/ingest.py` refuses and says so; `tests/test_topology.py::test_a_target_that_is_not_empty_is_refused_and_no_force_is_offered`, and the argument is the module's own docstring |
847
847
  | a `--force` on `taskops board rm` (and it is not an alias for `--discard-history`) | removing a board deletes the only copy of a history nobody can regenerate, and `--force` is a word every tool spends on something recoverable — it names no consequence, so it cannot warn. The flag that gets past the guardrail names the thing it destroys, and the guardrail itself is on the HOST, comparing the ids the caller says it holds against the ids the board really has: a wall the client enforces is a convention | `http/removal.py` refuses unless `core/holding.py` says the history is held elsewhere, and only a literal `true` opens `discard_history`; `tests/test_topology.py::test_the_flag_is_named_for_what_it_destroys_and_a_force_is_not_a_synonym` and `…::test_board_rm_refuses_a_history_this_checkout_does_not_hold_and_names_both_ways_out` |
848
+ | a `--force` push, a ref deletion, or a prune against the host's `repo.git` — and a HAND-ROLLED git protocol behind it | the host became the board's git remote (§16, "The host becomes the remote", 2026-08-31) precisely because a forge-side prune erased a landed card's diff from the board; a force or delete on the host would reintroduce the same erasure at the hub, and there is no flag that opens either, for `board rm`'s reason — `--force` names no consequence. And the pack protocol is git's own, spoken by git's plumbing through the one subprocess module, for the reason hand-rolled crypto is banned two rows up: a Python pkt-line is the same own-goal with a different file extension | `gitwork/bare.py` writes `receive.denyDeletes` and `receive.denyNonFastForwards` into the created repo's own config, so the refusing process IS the `git receive-pack` that would move the ref (`tests/test_topology.py::test_the_host_refuses_a_ref_deletion` and `…_a_non_fast_forward_even_forced` pin both, `--force` included); `subprocess` stays confined to `gitwork/run.py` by `tests/test_architecture.py` |
848
849
  | a SILENT `taskops join` over a local board | the local history stayed on disk byte for byte and nothing ever looked at it again or said so — a command about connecting is what made it invisible | `cli/commands.py::_keep_or_archive` refuses naming `taskops board push` and `--discard-local` (which archives, never deletes); `tests/test_topology.py::test_join_refuses_to_orphan_a_local_board_and_names_both_ways_out` |
849
850
  | GitHub as a CREDENTIAL — v1's GitHub login: a stored token, a call to GitHub on every sign-in | three costs, and §19 is the whole argument: a token that travels (worth stealing for reasons unrelated to the board), a network dependency at every login (GitHub down = nobody signs in), and a SECOND identity system beside the keys, with its own enrolment, expiry and revocation to keep in step. The convenience is kept and the costs are not: GitHub is asked ONCE, by the owner, and what persists is an ssh key | the only module that speaks to GitHub is `cli/github.py`, on the owner's machine; the host takes principals and key lines (`http/members.py`) and no token reaches it. `tests/test_topology.py::test_the_owners_token_is_spent_on_ONE_endpoint_and_written_nowhere` greps the host tree AND the checkout with a positive control, and `…::test_no_flag_on_join_takes_a_token_and_none_ever_will` holds the parser to "never a flag value". There is one `members.enroll` row in `core/scope.py` — a ROLE rule — and no second credential type anywhere |
850
851
  | a GitHub door on the DEV's side — `POST /<board>/join/github`, a `--github` flag, a token discovered at join time (2026-08-11) | it made every dev's own credential travel to a host that has no business seeing one, to prove a fact the OWNER already holds; and it asked for an ssh key from people who, having push over ssh, had published one already. Deleted the day after it shipped — §19.1 | there is no route, no flag and no client function: `grep -r 'join/github\|by_github' src tests` is empty, and `test_no_flag_on_join_takes_a_token_and_none_ever_will` asserts the ABSENCE from `join --help`. The replacement is one command on the owner's side and `taskops join`, bare, on the dev's (`…::test_the_dev_whose_key_the_sync_published_joins_with_two_words`) |
@@ -1722,6 +1723,14 @@ A repo joined to nothing behaves exactly as it did.
1722
1723
 
1723
1724
  ### The hosted window: the host may hold a MIRROR of the declared forge (decided 2026-08-30)
1724
1725
 
1726
+ *Reversed one day later — "The host becomes the remote", below. The direction
1727
+ of the git flow inverts: the host holds the board's OWN repo (`repo.git`),
1728
+ takes pushes from enrolled principals, and mirrors OUTWARD to the forge;
1729
+ `mirror.git` retires. This section stays as the argument it was, because what
1730
+ it got right (a hosted window must stand on a real history; the switch is
1731
+ decided at mount time; the credential is a deploy key, never a token)
1732
+ survives the reversal, and what it got wrong is named there.*
1733
+
1725
1734
  The first amendment narrowed "no server reads a repo" to "a host that sits in a
1726
1735
  repo may read it". This one narrows the other clause — *the server deliberately
1727
1736
  has no clone* — and, as before, the change is recorded here rather than quietly
@@ -1918,6 +1927,251 @@ sh smoke.sh # taskops.bernardocastro.dev, task
1918
1927
  sh smoke.sh <host> <board> <private-board>
1919
1928
  ```
1920
1929
 
1930
+ ### The host becomes the remote (decided 2026-08-31 — reverses the mirror)
1931
+
1932
+ This section has been narrowed twice and each time the change was recorded
1933
+ rather than quietly made. This amendment is larger: it REVERSES a ban rather
1934
+ than narrowing one, and the words it reverses are one day old. "The mirror
1935
+ takes no client pushes — the host still exposes no receive door of any kind"
1936
+ stops being true. From this chapter on:
1937
+
1938
+ > **The board's host holds the board's git, as a real remote. A checkout's
1939
+ > `origin` IS the host: `git push` lands there, `git clone`/`fetch` reads from
1940
+ > there, and the declared forge becomes an OUTBOUND mirror the host pushes to,
1941
+ > best effort. The host never prunes a card branch.**
1942
+
1943
+ **Be fair to the ban before reversing it.** "Client pushes to the host" was
1944
+ banned when the host had no git at all — a directory of boards, `subprocess`
1945
+ nowhere near it — and the failure it guarded against was real: two clones
1946
+ replicating git state between themselves is split-brain with a merge driver,
1947
+ and a host that accepted pushes with no identity story would have been an
1948
+ anonymous write with a packfile attached. Both hazards are still hazards.
1949
+ What changed is not the hazards; it is that the host now HAS an identity
1950
+ story every door already checks (§19), and that the topology proposed here is
1951
+ not replication between clones at all — it is a hub, one direction on each
1952
+ leg. Writing the ban down was right. What the day-old design got wrong is
1953
+ narrower and worth naming exactly.
1954
+
1955
+ **What the pull-mirror got wrong.** The mirror's one source was the forge, so
1956
+ the board's view of the code became a FUNCTION OF SOMEBODY ELSE'S BRANCH
1957
+ HYGIENE — three failures, all observed live against the hosted window on
1958
+ 2026-08-31, the day after it shipped:
1959
+
1960
+ - a worktree branch that was never pushed **does not exist** for the host, so
1961
+ a card being worked on had no diff at its URL until its close pushed it;
1962
+ - a `tk-*` branch pruned on the forge when its chapter landed **disappeared
1963
+ from the board**, even though the work is intact and reachable from master.
1964
+ Verified against production: `git ls-remote origin` had neither `tk-dfaff7`
1965
+ nor `ms/parallel-by-mechanism-the-board-`, yet both cards' work is in the
1966
+ trunk. The stale sentence called that "normal, not a fault" — true of the
1967
+ MIRROR, and exactly the fault of the DESIGN: a landed card's diff is the
1968
+ board's own record and it had been made erasable by a forge-side `git push
1969
+ --delete` the board never sees;
1970
+ - and structurally, the host's git was a copy of a copy: worktree → GitHub →
1971
+ mirror, with GitHub — the viewer — sitting in the middle of the board's own
1972
+ data path.
1973
+
1974
+ The original ask, restated by Berna the same day, was the other topology all
1975
+ along: the taskops server keeps the history and mirrors it ONWARD to GitHub.
1976
+ The cost of adopting it now is small by construction: `gitwork/bind.py`
1977
+ already pushes `origin <tk-…>` on post-commit, best effort, never a gate — so
1978
+ for a joined checkout the only thing that changes is WHICH remote `origin`
1979
+ is. The lifecycle pushes in `gitwork/remote.py::push` keep their contract and
1980
+ their ten seconds; they just stop travelling through a third party.
1981
+
1982
+ The decisions the rest of the chapter follows, each argued:
1983
+
1984
+ **Transport: git smart-HTTP over the existing door, not ssh.** The relay in
1985
+ front of this host (§17) exposes no port but HTTPS, so ssh transport would
1986
+ begin by re-plumbing the deployment. It would also mean a SECOND auth system
1987
+ — an sshd, an `authorized_keys`, a key-to-principal mapping — beside the one
1988
+ the board already has, for keys that are already enrolled on the host and
1989
+ already end in a bearer session every door checks (§19: one introduction, one
1990
+ credential). Git speaks smart-HTTP natively (`info/refs?service=…`, then
1991
+ `git-upload-pack` / `git-receive-pack` POSTs), authenticates it with HTTP
1992
+ Basic, and a session token rides in that password field with no client-side
1993
+ tooling at all. ssh becomes worth revisiting only if BOTH of these turn true:
1994
+ a deployment that fronts raw TCP (no relay), and clients that cannot hold a
1995
+ session token. Neither exists, and the second is unlikely ever to — `taskops
1996
+ join` mints the session that signs everything else.
1997
+
1998
+ **The address: `/<board>/repo.git`.** A git client is pointed at
1999
+ `https://<host>/<board>/repo.git` and everything git asks for lives under it:
2000
+ `/<board>/repo.git/info/refs?service=git-upload-pack`,
2001
+ `/<board>/repo.git/git-upload-pack`, `/<board>/repo.git/git-receive-pack`.
2002
+ It cannot collide with anything the router already serves, and each half of
2003
+ that is a fact about an existing closed set, not a hope: the JSON diff door
2004
+ is the tail `git/…` (and `api/git/…` — `http/routes.py::split` strips the
2005
+ prefix once, and `repo.git` does not start with `api/` so the strip never
2006
+ touches it), while `repo.git` is its own first segment; and the page's assets
2007
+ are `static.py::asset`'s closed set — flat filenames whose suffix is in
2008
+ `TYPES`, and `.git` is not in `TYPES`, so no bundle that ever ships can
2009
+ shadow the repo and the repo can never shadow an asset. The `.git` suffix is
2010
+ also the convention every forge trained every tool on, which is why the name
2011
+ is not `code` or `src`: an address a person guesses should be the one that
2012
+ works.
2013
+
2014
+ **On-disk: a NEW bare repo, `<root>/<board>/repo.git`, and `mirror.git`
2015
+ retires.** The mirror does not get promoted, for two reasons written into its
2016
+ own contract. It is DERIVED and disposable — "delete it and it re-clones" —
2017
+ and the board's real remote is the opposite thing: truth the forge does not
2018
+ necessarily hold, deletable by nobody. And its refspec is `--mirror`, whose
2019
+ fetch semantics are *make local match the forge, prunes included* — the exact
2020
+ behaviour this chapter exists to kill would be sitting in the repo's own
2021
+ config, one habitual `git fetch` away. A truth-holder configured to erase
2022
+ itself is not a truth-holder. So: a fresh `git init --bare repo.git` beside
2023
+ `events.jsonl`, and on a host that already has a populated `mirror.git`
2024
+ (production does, today), `repo.git` is SEEDED from it — one local
2025
+ `git clone --bare mirror.git repo.git`, on-disk and cheap — so no history the
2026
+ mirror held is lost, and the mirror-mode config dies with the old directory,
2027
+ which is then removed. Wired as `gitwork/bare.py::adopt` and called by
2028
+ `http/repos.py`, the first door that needs an answer: it is the ONE read path
2029
+ in the codebase allowed to write, argued there — a migration of a history the
2030
+ host already possesses is not a directory conjured by a stranger's question,
2031
+ and a board holding neither repo still leaves the disk untouched. `mirror.py`
2032
+ is DELETED in the same card rather than left as a second source; a fallback
2033
+ that could only ever answer for refs the FORGE has is wrong in exactly the case
2034
+ the reader asks about (a branch nobody pushed, a branch the forge pruned), and
2035
+ `http/repos.py` carries that argument. `http/repos.py` points `/git` and the
2036
+ hosted window at `repo.git` from then on: the window's diffs are read from the board's OWN
2037
+ repository, with no dependency on the forge — which is the acceptance the
2038
+ chapter is held to.
2039
+
2040
+ **Roles: a push is a write, and §11's anonymous-write ban covers it.** Only
2041
+ an enrolled principal may push — `dev:<name>` and `agent:<dev>/<name>` alike,
2042
+ because workers are exactly who `bind.py` pushes as, on every commit. An
2043
+ anonymous caller and a read-only credential are refused at the receive door
2044
+ with the sentence that names `taskops join`. A read — clone, fetch,
2045
+ `ls-remote` — follows the board's VISIBILITY exactly as `/rpc` does: a public
2046
+ board clones anonymously (and, per §11's invisible-write ban, that read
2047
+ leaves no `presence` row), a private board clones with the credential the
2048
+ doors already check. One rule, already written, now asked by one more door.
2049
+
2050
+ **The host never prunes, and never takes a force.** A card branch pushed to
2051
+ the host is readable at its URL forever — that permanence is the point of the
2052
+ reversal, so it is enforced, not hoped: the receive door refuses a ref
2053
+ deletion and a non-fast-forward push outright. There is no flag that opens
2054
+ either, for `board rm`'s reason (§11): `--force` is a word that names no
2055
+ consequence, and what it would destroy here is the diff of a landed card —
2056
+ the board's own record. History rewriting, when it is ever needed, is the
2057
+ owner's deliberate act against the host's filesystem, not a verb.
2058
+
2059
+ **The outbound mirror: best effort, never a gate, failure visible.** The
2060
+ forge relationship inverts, it does not end: a board that declared one
2061
+ (`taskops board forge <owner>/<repo>`) gets its history pushed ONWARD, from
2062
+ `repo.git` to the forge, at the same lifecycle moments `remote.py::push`
2063
+ already owns — and a forge that is down MUST NOT fail the client's push,
2064
+ which lands on the host and is done. Best effort never means silent: a mirror
2065
+ push that fails is a FACT a reader can see — surfaced where the board already
2066
+ surfaces derived facts, on the payload, never swallowed into a log line —
2067
+ because an invisible best-effort is how the forge quietly drifts a month
2068
+ behind. The credential is an ssh deploy key on the host's filesystem with
2069
+ WRITE on the forge — and that is a real escalation over the mirror's
2070
+ read-only key, so it is named as one: minted and installed by the OWNER, an
2071
+ explicit act, revocable on the forge like any deploy key, and scoped to that
2072
+ one repo. §19.2 carries the credential argument. What it is NOT: a GitHub
2073
+ token (the key opens one repo, not an account), and never anything a dev
2074
+ supplies.
2075
+
2076
+ Wired as `gitwork/onward.py`, started by the receive door once git's own exit
2077
+ code says the push landed (`http/gitpack.py`) and running on a background
2078
+ thread — best effort in TIME as well as in outcome, since inline the client
2079
+ would feel the mirror as a ten-second gate on a push that already succeeded.
2080
+ Two pushes racing need no lock: the refspec is the whole `refs/heads/*`, so a
2081
+ later push subsumes an earlier one; only the REPORT is ordered, by timestamp
2082
+ (`store/mirroring.py`). The report is a row in `live.sqlite` — alive, never an
2083
+ event, because "could this host reach GitHub" is false the moment the log
2084
+ travels — and it rides on the board payload as `mirror` (`verbs/pulse.py`,
2085
+ under `forge`'s own contract: nothing to say sends no key), drawn as one
2086
+ MIRROR line by `mcp/boardview.py`, success included, so "silence" can never
2087
+ mean both "up to date" and "the key expired a month ago". The outbound remote
2088
+ is one named `forge` inside `repo.git`, added by the owner by hand — the ssh
2089
+ address IS the credential story, and a forge declared with no remote reads as
2090
+ a failure naming that exact command.
2091
+
2092
+ **A board with NO declared forge gets everything except the outbound
2093
+ mirror — and that is not a fault.** Push, clone, the hosted window's diffs,
2094
+ the permanence: all of it stands on `repo.git` alone. The forge was the
2095
+ SOURCE under the pull-mirror, so a board without one had no hosted window at
2096
+ all; under this topology the forge is a projection, and a board that wants no
2097
+ projection is simply complete. `NO_FORGE`'s refusal retires with the mirror
2098
+ it described — into `gitdoor.NO_REPO`, which now says the true board-level
2099
+ fact ("nobody has pushed this board's code here yet") and names the two moves
2100
+ that end it. The HOSTED WINDOW's gate moves the same way: `repos.backed` asks
2101
+ for `repo.git`, not for a forge, so `static.NO_UI` stops promising a page in
2102
+ exchange for `taskops board forge` — and the missing-ref sentence's AUDIENCE
2103
+ split (tk-9cde88) outlives the mirror that prompted it, the host's half
2104
+ rewritten as `stale.HOSTED`: this host prunes nothing, so a ref absent here was
2105
+ never pushed here. The window's half is unchanged, byte for byte.
2106
+
2107
+ **Beside §20, not inside it.** `board push`/`board pull` move the BOARD — the
2108
+ event log, the one history nobody can regenerate — and this chapter moves the
2109
+ CODE. The two lifecycles share a host and nothing else: `board push` still
2110
+ promotes a history exactly once onto an empty target, `board pull` is still a
2111
+ snapshot, `board rm` still guards on possession — and `board rm` now names
2112
+ one more thing it destroys, the board's `repo.git`, judged by the same
2113
+ possession logic: a history this checkout holds (the clone IS possession of
2114
+ the git half) is deletable, one it does not hold is refused.
2115
+
2116
+ **What stays banned, spelled out, because a reversal invites the question:**
2117
+
2118
+ - **replication BETWEEN clones.** This is a hub — worktree → host, host →
2119
+ forge, one direction on each leg. No clone pushes to another clone, no
2120
+ clone pulls the board's state from anywhere but the host, and the host
2121
+ never pulls git from the forge again.
2122
+ - **a stored GitHub token, and GitHub as a second credential type.** §19,
2123
+ unmoved. The outbound key is a deploy key: one repo, revocable, the
2124
+ owner's.
2125
+ - **anonymous writes, in any form — a git push included.** The receive door
2126
+ is behind the same credential wall as every write verb.
2127
+ - **a hand-rolled git protocol.** The pack protocol is git's, spoken by
2128
+ git's own plumbing through the ONE subprocess module (`gitwork/run.py`),
2129
+ the way SSHSIG verification is OpenSSH's (§11). A Python reimplementation
2130
+ of pkt-line is the crypto own-goal with a different file extension.
2131
+ - **a `--force` or a prune that could erase a card branch on the host.**
2132
+ Argued above; it is the reversal's own reason turned into a rule.
2133
+
2134
+ ### The dev's side: `taskops remote git`, and no `origin` (2026-08-31)
2135
+
2136
+ The reversal gave the host a git door and left the client unable to NAME it:
2137
+ `https://<host>/<board>/repo.git` existed only inside `http/gitpack.py`, so
2138
+ pointing a checkout at it meant reading the source. `taskops remote git` is
2139
+ that sentence — printed to paste, or `--add` to write it here — and it is a
2140
+ third action on `taskops remote` rather than a twelfth top-level command,
2141
+ because recording where the board's git lives is the same act as recording
2142
+ where the board lives (`cli/gitremote.py`).
2143
+
2144
+ **`origin` is never written, and `--name origin` is refused outright.** Not
2145
+ "left alone if it exists": refused. A checkout's `origin` is somebody's own
2146
+ setup — very often their GitHub — and no state of it is this command's to
2147
+ decide, its absence included. The remote is `taskops`; a name already in use
2148
+ is a refusal that names `--name <other>`, never a `set-url`. Adding a remote
2149
+ is CONNECTING (the CLI's promise); repointing one is managing.
2150
+
2151
+ **The credential is a HELPER, never a URL.** `https://x:<token>@host/…` is the
2152
+ obvious spelling and it is banned here for two reasons, either sufficient:
2153
+ `git remote add` persists its URL in `.git/config`, plaintext, so a live
2154
+ session token would sit in the one file nobody thinks to look at; and a
2155
+ taskops session EXPIRES, so a token baked into a config is wrong within the
2156
+ hour and cannot renew itself. What gets configured is
2157
+ `credential.<host>.helper`, pointing at `taskops hook credential` — git asks
2158
+ for a password at push time, the helper mints or renews a session from the ssh
2159
+ key already on disk (`identity.establish`, the same entry every board verb
2160
+ uses) and hands it over on a pipe. `store` and `erase` are answered with
2161
+ silence: a helper that caches is a helper that persists a token, and minting is
2162
+ free. One credential story, §19's, reaching git through git's own door — and
2163
+ `allowed_signers` remains the only thing the host keeps.
2164
+
2165
+ **The address is NOT a board-payload field, and that is the argued half.** The
2166
+ payload carries the forge and the mirror fact, so `repo_url` looks like it
2167
+ belongs beside them. It does not: the server cannot know its own public
2168
+ address — a proxy, a port-forward, the local `taskops ui` window, and every
2169
+ reader already reached it at an address the server never saw — so a stored one
2170
+ is the first field to rot and the first to send a dev to the wrong host. The
2171
+ address is (the host you asked) + `/<board>/repo.git`, a derivation the CLIENT
2172
+ can always do correctly and the server never can. `board ls` prints that shape
2173
+ once, and names the command that spells it exactly.
2174
+
1921
2175
  ---
1922
2176
 
1923
2177
  ## 17. Deployed — taskops.bernardocastro.dev is v2 (2026-08-08)
@@ -2473,6 +2727,36 @@ which `as_object` flattens to `{}`. A page of people silently reading as nobody
2473
2727
  is the bug that shape prevents, so the decoding belongs to whoever knows what it
2474
2728
  asked for and only the status and `Unreachable` are shared.
2475
2729
 
2730
+ ### 19.2 The mirror's key gains WRITE — the owner's explicit act (2026-08-31)
2731
+
2732
+ The hosted-window chapter (§16, 2026-08-30) added exactly one credential to
2733
+ this story: an ssh deploy key on the host's filesystem, READ-ONLY, so the
2734
+ host could pull a private repo's mirror. The reversal (§16, "The host becomes
2735
+ the remote") inverts the flow, and the key follows: the host now PUSHES to
2736
+ the declared forge, so the deploy key needs write on that one repo. That is a
2737
+ real escalation and it is treated as one, not smuggled in as a config detail:
2738
+
2739
+ - **It is the owner's explicit act, twice over.** Declaring the forge was
2740
+ already the owner's move (`taskops board forge`); minting a WRITE deploy
2741
+ key and installing it on the host is a second, separate decision the owner
2742
+ makes with eyes open, because it hands the host the power to move branches
2743
+ on the forge. Nothing mints it, discovers it, or asks a dev for it.
2744
+ - **It is still not a token, and the difference still matters.** A deploy key
2745
+ opens ONE repo; a GitHub token opens an account. If the host is ever
2746
+ compromised, the blast radius is the mirror of a repo whose real history
2747
+ the host itself holds — revoke the key on the forge and the leg is dead,
2748
+ the board untouched. Everything §19 bans stays banned: no stored GitHub
2749
+ token, no dev credential travelling, no second identity system.
2750
+ - **Write on the forge, never force.** The outbound push is the same
2751
+ best-effort, fast-forward-only discipline the host demands of its own
2752
+ receive door: the mirror leg cannot delete or rewrite on the forge either,
2753
+ so a key with write is not a key that erases.
2754
+
2755
+ A board whose owner declines the escalation keeps the read side whole: the
2756
+ host serves clones, pushes, and the hosted window from `repo.git`, and the
2757
+ forge simply goes unmirrored — visible as the mirror-push fact the payload
2758
+ carries, never as a fault.
2759
+
2476
2760
  ## 20. A board's whole life — create → push → (live) → pull → rm (2026-08-10)
2477
2761
 
2478
2762
  Until this chapter a board's life ran one way. `board create` made one, `board
@@ -3,6 +3,57 @@
3
3
  The source of truth for release notes — GitHub Releases are extracted from
4
4
  here, never written twice.
5
5
 
6
+ ## 0.5.2 — the host holds the git; GitHub is the copy
7
+
8
+ 0.5.0 and 0.5.1 built the hosted window on a mirror the host PULLED from the
9
+ declared forge. That was the wrong direction, and the reader felt it: a
10
+ worktree branch nobody had pushed did not exist for the host, and a `tk-*`
11
+ branch pruned when its chapter landed erased a finished card's diff even
12
+ though the commits were sitting in the trunk. The board's view of the code was
13
+ a function of somebody else's branch hygiene. This release inverts it.
14
+
15
+ - **The host IS the board's git remote.** `<root>/<board>/repo.git`, served
16
+ over smart-HTTP at `https://<host>/<board>/repo.git`: clone, fetch and push.
17
+ A worktree's card branch reaches it at the moment of the commit — the
18
+ post-commit hook already pushed `origin <tk-…>`, so for a joined checkout the
19
+ only thing that changes is which remote that is.
20
+ - **It never prunes.** `receive.denyDeletes` and `receive.denyNonFastForwards`
21
+ live in the repo's own config, so the refusal comes from the very
22
+ `receive-pack` process that would move the ref: no window between an
23
+ application check and the update, no hook script a later `git init` can drop,
24
+ and a client's `--force` changes nothing. A landed card's diff is readable at
25
+ its URL forever.
26
+ - **GitHub becomes the outbound copy.** After a push lands, the host pushes
27
+ onward to the declared forge on a background thread — best effort in outcome
28
+ AND in time, since inline a hanging forge would make the mirror felt as a
29
+ ten-second gate on a push that already succeeded. Fast-forward only: no
30
+ `--mirror` (it deletes), no `--force`, no `--prune`. The host does not do to
31
+ the forge what it refuses for itself.
32
+ - **A mirror failure is never silent.** Success and failure both land on the
33
+ board payload as `mirror` and on the orchestrator's board view — "up to date,
34
+ pushed 2m ago" or "FAILED just now: <git's own words>" — kept in
35
+ `live.sqlite` rather than the event log, because "this host reached GitHub ten
36
+ seconds ago" replayed on another machine is a lie.
37
+ - **One credential, still.** A push authenticates as the enrolled principal the
38
+ board already knows: git's HTTP Basic carries a session in the password
39
+ field, minted at push time from the dev's ssh key by a credential helper
40
+ (`taskops hook credential`), so no token is ever written into `.git/config`.
41
+ The receive door answers 401 with `WWW-Authenticate: Basic`, because git only
42
+ volunteers a credential after a challenge. Reads follow the board's
43
+ visibility exactly as `/rpc` does.
44
+ - **`taskops remote git [--add]`** wires a checkout to the board's git and
45
+ prints what it did. It refuses `--name origin` outright: repointing somebody's
46
+ own remote is not connecting, it is rewriting. `taskops join` does not do it
47
+ for you — it is the one act that could silently change where a push goes.
48
+ - **The pull mirror is deleted**, not kept as a fallback: it could only ever
49
+ answer for refs the forge has, which is wrong in exactly the case a reader
50
+ asks about. A host that already holds a populated `mirror.git` migrates it
51
+ once — `git clone --bare` into `repo.git`, then the mirror is removed.
52
+ - **The owner's two acts**, if they want GitHub kept in sync: a WRITE deploy key
53
+ on the host, and `git -C <root>/<board>/repo.git remote add forge
54
+ git@github.com:<owner>/<repo>.git`. Until then every board read says the
55
+ mirror is not configured. Still no stored GitHub token, anywhere.
56
+
6
57
  ## 0.5.1 — the hosted window, actually usable
7
58
 
8
59
  0.5.0 shipped a window nobody could use. Both faults were reported the next
@@ -110,6 +110,46 @@ payload, derived per read exactly as `visibility` is, so a reader finds the door
110
110
  instead of bumping into it. **A board with no forge sends no key at all** —
111
111
  never `null`.
112
112
 
113
+ **The HOST holds the git; GitHub is the copy** (2026-08-31, ARCHITECTURE §16
114
+ "The host becomes the remote", which REVERSES the one-day-old pull mirror — read
115
+ that section before touching any of it). One hub, one direction per leg:
116
+ worktree → host → forge.
117
+
118
+ ```
119
+ your worktree ──push──▶ <root>/<board>/repo.git ──best effort──▶ the declared forge
120
+ ```
121
+
122
+ - The door is git smart-HTTP at `/<board>/repo.git/…` (`http/gitpack.py`), spoken
123
+ by git's own plumbing through the ONE subprocess module. A session token rides
124
+ in HTTP Basic's password field; a refusal there is a **401 with
125
+ `WWW-Authenticate`**, not `/rpc`'s 409, because git volunteers Basic only after
126
+ one.
127
+ - A READ follows the board's visibility (public clones anonymously, and leaves no
128
+ `presence` row); a PUSH is a write, so only an enrolled principal may make one.
129
+ - **The host never prunes and takes no force**: `receive.denyDeletes` /
130
+ `denyNonFastForwards` in the repo's own config, and no flag opens either. A
131
+ card branch is readable at its URL after the forge pruned it — that permanence
132
+ IS the chapter.
133
+ - The bare repo is created on the first push and a legacy `mirror.git` is
134
+ MIGRATED, not promoted (`gitwork/bare.py::adopt`; `gitwork/mirror.py` is
135
+ deleted). `http/repos.py` is the one read path allowed to write, argued there.
136
+ - The outbound leg is `gitwork/onward.py`, started by the receive door once git's
137
+ exit code says the push landed, on a background thread: **best effort, never a
138
+ gate, never silent** — the outcome is a `live.sqlite` row (`store/mirroring.py`)
139
+ riding on the board payload as `mirror`, drawn as one MIRROR line. Its
140
+ credential is a WRITE deploy key the OWNER installs on the host plus a remote
141
+ named `forge` inside `repo.git`, added by hand (§19.2). Still not a token.
142
+ - **A board with no forge gets everything except the outbound leg** — push,
143
+ clone, the hosted window's diffs, the permanence all stand on `repo.git`
144
+ alone — so `NO_FORGE` retired into `gitdoor.NO_REPO` / `static.NO_UI`, which
145
+ name a board nobody has pushed to yet, never a missing forge.
146
+ - The DEV's side is `taskops remote git [--add]` (`cli/gitremote.py`): it writes
147
+ a remote named `taskops` and **refuses `--name origin` outright**, and the
148
+ credential is a HELPER (`taskops hook credential`), never a URL — `git remote
149
+ add` persists its URL in `.git/config` and a session expires within the hour.
150
+ The address is not a board-payload field: the server cannot know its own public
151
+ address, so it is (the host you asked) + `/<board>/repo.git`, derived client-side.
152
+
113
153
  ## Layers — imports only point DOWN
114
154
 
115
155
  ```
@@ -150,6 +190,8 @@ a *library* users must not import, and taskops has no such half.
150
190
  <board>/events.jsonl THE TRUTH — append + fsync BEFORE the cache
151
191
  <board>/cache.sqlite derived, disposable (delete it, it rebuilds)
152
192
  <board>/live.sqlite leases + presence — separate file ON PURPOSE
193
+ <board>/repo.git the board's OWN git, on the HOST — created by the first
194
+ push, never pruned, never force-pushed. Truth, not a cache
153
195
  <root>/server.sqlite the HOST: principals + pubkeys
154
196
  <root>/allowed_signers DERIVED from it, whole, on every change
155
197
  ```
@@ -160,16 +202,19 @@ releases. A board is created by an explicit act and never by being asked for.
160
202
 
161
203
  ## The CLI surface — it connects, it never manages
162
204
 
163
- Eleven top-level commands, and `board` is the only one with actions of its own.
164
- Moving a card from the terminal does not exist: that is MCP. Re-derive both
165
- lists rather than trusting this paragraph — `--help` is the source:
205
+ Eleven top-level commands; two of them — `board` and `remote` — have actions of
206
+ their own. Moving a card from the terminal does not exist: that is MCP.
207
+ Re-derive every list rather than trusting this paragraph — `--help` is the source:
166
208
 
167
209
  ```sh
168
210
  uv run python -m taskops.cli --help | sed -n '/^usage/,/^$/p' # the eleven
169
211
  uv run python -m taskops.cli board --help # its actions
212
+ uv run python -m taskops.cli remote --help # add · git
170
213
  ```
171
214
 
172
- `board` today: `create · ls · push · pull · rm · visibility · forge`. The four that move
215
+ `board` today: `create · ls · push · pull · rm · visibility · forge`; `remote`:
216
+ `add` (the host this checkout operates) and `git` (the board's own `repo.git`,
217
+ printed or `--add`ed as a remote named `taskops`). The four that move
173
218
  a whole history are one lifecycle, and each says what it destroys:
174
219
 
175
220
  ```
@@ -230,20 +275,25 @@ what it exists to report, not a fault.
230
275
 
231
276
  **The hosted page is at the board's OWN address** — `https://<host>/<board>/`
232
277
  (and `/<board>`), its assets beside it, the machine doors under
233
- `/<board>/api/{rpc,git,feed}`. 0.5.0's spellings (`/<board>/ui/`,
278
+ `/<board>/api/{rpc,git,feed}` — and the board's git is its own first segment,
279
+ `/<board>/repo.git/…`, which can collide with neither (the `api/` strip never
280
+ touches it, and `.git` is not in `static.py`'s asset `TYPES`). Its diffs are read
281
+ from that repo, never from a forge. 0.5.0's spellings (`/<board>/ui/`,
234
282
  `/<board>/rpc`, `/<board>/git/…`, `/<board>/feed`) still answer and are a
235
283
  contract, not a legacy — ARCHITECTURE §16, "The board's own address IS the
236
284
  page". The page derives its base from its own location
237
285
  (`ui/src/client.ts::baseOf`) and hardcodes neither. `sh smoke.sh` re-derives all
238
286
  of it against the live host, plus the credential rule a browser exposed:
239
287
  **an `Authorization` header with no value is the ABSENCE of a credential, a
240
- header with a wrong value still refuses loudly** (`http/auth.py::token_in`).
288
+ header with a wrong value still refuses loudly** (`http/auth.py::token_in`) —
289
+ and the git doors, whose anonymous push must come back `401` +
290
+ `WWW-Authenticate` rather than a 409.
241
291
 
242
292
  ## Never re-introduce
243
293
 
244
294
  Each has its line in ARCHITECTURE.md §11 saying what it cost and where it is
245
295
  enforced: a reviewer ROLE, a stored review STATUS, or automatic reviewer
246
- assignment · `land` or automatic merges to the trunk · a SECOND trunk (2026-08-10: `main` and `master` both existed, `trees.base_ref` cut every chapter from `origin/main`, and a one-sided push refspec was quietly landing card merges there — three facts that only became a bug together) · git replication between clones
296
+ assignment · `land` or automatic merges to the trunk · a SECOND trunk (2026-08-10: `main` and `master` both existed, `trees.base_ref` cut every chapter from `origin/main`, and a one-sided push refspec was quietly landing card merges there — three facts that only became a bug together) · git replication BETWEEN CLONES (the host-as-remote hub is not that: one direction per leg, and the host never pulls git from the forge)
247
297
  · Claude hooks **that decide or store** · a stored `doing` · a slug in a branch
248
298
  name · a `recover` · a mark-as-read/ack verb · per-request SIGNING · hand-rolled
249
299
  crypto or a pip crypto dependency · a `--force` on `board push` **or on `board