taskops-cli 0.5.1__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ARCHITECTURE.md +300 -5
  2. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/CHANGELOG.md +77 -0
  3. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/CLAUDE.md +57 -7
  4. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/PKG-INFO +80 -9
  5. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/README.md +79 -8
  6. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/smoke.sh +91 -10
  7. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_version.py +1 -1
  8. taskops_cli-0.5.3/src/taskops/cli/gitremote.py +198 -0
  9. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/main.py +10 -1
  10. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/operate.py +2 -2
  11. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/parser.py +18 -3
  12. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/remote.py +11 -1
  13. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/forge.py +10 -0
  14. taskops_cli-0.5.3/src/taskops/gitwork/bare.py +122 -0
  15. taskops_cli-0.5.3/src/taskops/gitwork/onward.py +168 -0
  16. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/run.py +83 -1
  17. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/auth.py +22 -0
  18. taskops_cli-0.5.3/src/taskops/http/gitbody.py +69 -0
  19. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/gitdoor.py +30 -28
  20. taskops_cli-0.5.3/src/taskops/http/gitpack.py +176 -0
  21. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/handler.py +16 -33
  22. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/mounts.py +3 -3
  23. taskops_cli-0.5.3/src/taskops/http/page.py +42 -0
  24. taskops_cli-0.5.3/src/taskops/http/repos.py +109 -0
  25. taskops_cli-0.5.3/src/taskops/http/stale.py +114 -0
  26. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/static.py +24 -19
  27. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/boardview.py +20 -0
  28. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/live.py +5 -0
  29. taskops_cli-0.5.3/src/taskops/store/mirroring.py +96 -0
  30. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/ui/app.js +9 -9
  31. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/pulse.py +5 -0
  32. taskops_cli-0.5.3/tests/test_mirror.py +264 -0
  33. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_topology.py +760 -119
  34. taskops_cli-0.5.3/ui/smoke/sections/card-diff-range.tsx +119 -0
  35. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/git-diff.tsx +1 -1
  36. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Patch.tsx +34 -21
  37. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Sections.tsx +17 -15
  38. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/links.tsx +77 -0
  39. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/WorktreeDiff.tsx +5 -2
  40. taskops_cli-0.5.1/src/taskops/gitwork/mirror.py +0 -132
  41. taskops_cli-0.5.1/src/taskops/http/repos.py +0 -98
  42. taskops_cli-0.5.1/src/taskops/http/stale.py +0 -62
  43. taskops_cli-0.5.1/tests/test_mirror.py +0 -164
  44. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.claude/settings.json +0 -0
  45. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.gitattributes +0 -0
  46. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.github/workflows/ci.yml +0 -0
  47. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.github/workflows/workflow.yml +0 -0
  48. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.gitignore +0 -0
  49. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.mcp.json +0 -0
  50. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/cache.sqlite +0 -0
  51. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/cache.sqlite-shm +0 -0
  52. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/cache.sqlite-wal +0 -0
  53. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/events.jsonl +0 -0
  54. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/live.sqlite +0 -0
  55. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/live.sqlite-shm +0 -0
  56. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/live.sqlite-wal +0 -0
  57. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board.json +0 -0
  58. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/reports/lifecycle-chapter.html +0 -0
  59. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/reports/reports-chapter.html +0 -0
  60. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/ui.lock +0 -0
  61. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/pyproject.toml +0 -0
  62. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/__init__.py +0 -0
  63. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_clock.py +0 -0
  64. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_errors.py +0 -0
  65. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_ids.py +0 -0
  66. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_json.py +0 -0
  67. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_locate.py +0 -0
  68. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_wire.py +0 -0
  69. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/board.py +0 -0
  70. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/__init__.py +0 -0
  71. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/__main__.py +0 -0
  72. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/admin.py +0 -0
  73. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/claude.py +0 -0
  74. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/commands.py +0 -0
  75. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/enrol.py +0 -0
  76. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/github.py +0 -0
  77. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/grants.py +0 -0
  78. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/hooks.py +0 -0
  79. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/paging.py +0 -0
  80. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/pull.py +0 -0
  81. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/push.py +0 -0
  82. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/rm.py +0 -0
  83. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/serving.py +0 -0
  84. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/team.py +0 -0
  85. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/watch.py +0 -0
  86. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/window.py +0 -0
  87. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/wording.py +0 -0
  88. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/__init__.py +0 -0
  89. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/actors.py +0 -0
  90. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/challenge.py +0 -0
  91. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/chapters.py +0 -0
  92. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/event.py +0 -0
  93. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/graph.py +0 -0
  94. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/holding.py +0 -0
  95. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/hours.py +0 -0
  96. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/kinds.py +0 -0
  97. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/machine.py +0 -0
  98. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/mentions.py +0 -0
  99. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/replay.py +0 -0
  100. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/reports.py +0 -0
  101. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/review.py +0 -0
  102. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/scope.py +0 -0
  103. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/seams.py +0 -0
  104. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/types.py +0 -0
  105. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/__init__.py +0 -0
  106. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/bind.py +0 -0
  107. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/catchup.py +0 -0
  108. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/claudefiles.py +0 -0
  109. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/diff.py +0 -0
  110. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/install.py +0 -0
  111. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/landing.py +0 -0
  112. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/patch.py +0 -0
  113. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/remote.py +0 -0
  114. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/sig.py +0 -0
  115. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/trailer.py +0 -0
  116. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/trees.py +0 -0
  117. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/__init__.py +0 -0
  118. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/admin.py +0 -0
  119. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/feed.py +0 -0
  120. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/grants.py +0 -0
  121. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/ingest.py +0 -0
  122. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/login.py +0 -0
  123. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/members.py +0 -0
  124. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/removal.py +0 -0
  125. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/routes.py +0 -0
  126. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/rpc.py +0 -0
  127. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/scoped.py +0 -0
  128. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/server.py +0 -0
  129. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/upstream.py +0 -0
  130. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/watcher.py +0 -0
  131. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/identity.py +0 -0
  132. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/__init__.py +0 -0
  133. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/__main__.py +0 -0
  134. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/activity.py +0 -0
  135. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/before.py +0 -0
  136. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/boards.py +0 -0
  137. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/brief.py +0 -0
  138. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/chapter.py +0 -0
  139. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/dossier.py +0 -0
  140. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/fields.py +0 -0
  141. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/gitmoves.py +0 -0
  142. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/hello.py +0 -0
  143. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/integrate.py +0 -0
  144. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/orders.py +0 -0
  145. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/render.py +0 -0
  146. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/schema.py +0 -0
  147. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/server.py +0 -0
  148. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/thread.py +0 -0
  149. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/tools.py +0 -0
  150. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/py.typed +0 -0
  151. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/session.py +0 -0
  152. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/__init__.py +0 -0
  153. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/cache.py +0 -0
  154. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/creds.py +0 -0
  155. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/handover.py +0 -0
  156. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/log.py +0 -0
  157. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/pubkeys.py +0 -0
  158. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/reviews.py +0 -0
  159. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/server.py +0 -0
  160. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/stores.py +0 -0
  161. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/ui/index.html +0 -0
  162. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/ui/style.css +0 -0
  163. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/__init__.py +0 -0
  164. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_args.py +0 -0
  165. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_cards.py +0 -0
  166. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_chapter.py +0 -0
  167. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_context.py +0 -0
  168. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_facts.py +0 -0
  169. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_mentions.py +0 -0
  170. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_rows.py +0 -0
  171. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_stories.py +0 -0
  172. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_waiting.py +0 -0
  173. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_windows.py +0 -0
  174. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/activity.py +0 -0
  175. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/assign.py +0 -0
  176. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/card.py +0 -0
  177. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/events.py +0 -0
  178. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/filed.py +0 -0
  179. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/plan.py +0 -0
  180. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/project.py +0 -0
  181. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/record.py +0 -0
  182. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/report.py +0 -0
  183. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/review.py +0 -0
  184. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/take.py +0 -0
  185. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/update.py +0 -0
  186. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/__init__.py +0 -0
  187. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/conftest.py +0 -0
  188. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_architecture.py +0 -0
  189. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_claude.py +0 -0
  190. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_core.py +0 -0
  191. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_git.py +0 -0
  192. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_mcp.py +0 -0
  193. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_report.py +0 -0
  194. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_store.py +0 -0
  195. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_ui.py +0 -0
  196. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_verbs.py +0 -0
  197. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_window.py +0 -0
  198. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/ui_harness.js +0 -0
  199. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/build.mjs +0 -0
  200. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/index.html +0 -0
  201. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/package-lock.json +0 -0
  202. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/package.json +0 -0
  203. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/fixture.json +0 -0
  204. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/main.tsx +0 -0
  205. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/run.mjs +0 -0
  206. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/actors-window-filter.tsx +0 -0
  207. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/actors.tsx +0 -0
  208. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/all-chapters-scope.tsx +0 -0
  209. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-columns.tsx +0 -0
  210. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-flow-view.tsx +0 -0
  211. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
  212. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-root-mount.tsx +0 -0
  213. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
  214. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapter-landed.tsx +0 -0
  215. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
  216. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapters-open.tsx +0 -0
  217. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/comment-box.tsx +0 -0
  218. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
  219. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
  220. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/diff-page.tsx +0 -0
  221. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/dossier.tsx +0 -0
  222. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/event-stream.tsx +0 -0
  223. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/feed-reconnect.tsx +0 -0
  224. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/forge-links.tsx +0 -0
  225. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
  226. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/hosted-window.tsx +0 -0
  227. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/monitor-panes.tsx +0 -0
  228. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/overlay-escape.tsx +0 -0
  229. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/report-markdown.tsx +0 -0
  230. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/report-sandbox.tsx +0 -0
  231. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/reviewing-skew.tsx +0 -0
  232. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/section.ts +0 -0
  233. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/swarm-topology.tsx +0 -0
  234. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/thread-closing-note.tsx +0 -0
  235. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/worktrees-index.tsx +0 -0
  236. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections.mjs +0 -0
  237. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/App.tsx +0 -0
  238. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/client.ts +0 -0
  239. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/completed.ts +0 -0
  240. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/actors/Daysheet.tsx +0 -0
  241. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/actors/DevPanel.tsx +0 -0
  242. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/CardTile.tsx +0 -0
  243. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/Column.tsx +0 -0
  244. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/ViewToggle.tsx +0 -0
  245. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/flip.ts +0 -0
  246. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/useFlip.ts +0 -0
  247. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/CommentBox.tsx +0 -0
  248. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Drawer.tsx +0 -0
  249. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Thread.tsx +0 -0
  250. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/split.ts +0 -0
  251. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/tokens.ts +0 -0
  252. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/AvatarStack.tsx +0 -0
  253. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/Header.tsx +0 -0
  254. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/KpiRail.tsx +0 -0
  255. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
  256. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/TabNav.tsx +0 -0
  257. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/flow/FlowView.tsx +0 -0
  258. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/flow/layout.ts +0 -0
  259. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Chapter.tsx +0 -0
  260. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/DependencyChain.tsx +0 -0
  261. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/EditSurface.tsx +0 -0
  262. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/EventStream.tsx +0 -0
  263. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
  264. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/LiveLeases.tsx +0 -0
  265. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Mentions.tsx +0 -0
  266. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Pane.tsx +0 -0
  267. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Swarm.tsx +0 -0
  268. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Throughput.tsx +0 -0
  269. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/panels.ts +0 -0
  270. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/reports/ReportFrame.tsx +0 -0
  271. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Avatar.tsx +0 -0
  272. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Markdown.tsx +0 -0
  273. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Overlay.tsx +0 -0
  274. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Segmented.tsx +0 -0
  275. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/overlayStack.ts +0 -0
  276. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/story/ChapterStory.tsx +0 -0
  277. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/story/stats.ts +0 -0
  278. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/toasts/ToastStack.tsx +0 -0
  279. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/toasts/model.ts +0 -0
  280. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/toasts/useToasts.ts +0 -0
  281. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/format.ts +0 -0
  282. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/hoursWindow.ts +0 -0
  283. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/main.tsx +0 -0
  284. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/markdown.ts +0 -0
  285. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Actors.tsx +0 -0
  286. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Board.tsx +0 -0
  287. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Monitor.tsx +0 -0
  288. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Reports.tsx +0 -0
  289. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Worktrees.tsx +0 -0
  290. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/theme/theme.ts +0 -0
  291. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/theme/tokens.css +0 -0
  292. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/types.ts +0 -0
  293. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/useBoard.ts +0 -0
  294. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/useEvents.ts +0 -0
  295. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/styles/index.css +0 -0
  296. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/tsconfig.json +0 -0
  297. {taskops_cli-0.5.1 → taskops_cli-0.5.3}/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`) |
@@ -1525,9 +1526,16 @@ below — and comes back only standing on a real history: the forge mirror, in
1525
1526
  That cascade is ONE function — `ui/src/links.tsx::cascade`, beside the slug and
1526
1527
  the link templates it already owned — and `ui/src/components/card/Patch.tsx`
1527
1528
  only DRAWS the step it is handed, so no component holds a fallback order of its
1528
- own. It feeds the dossier's **Files changed** (the card as a PR:
1529
- `compare/ms/<slug>...tk-<id>`, the file list from `stat`, each file's patch on
1530
- expand via `?path=`) and the fold on every commit row. Availability is
1529
+ own. It feeds the dossier's **Files changed** (the card as a PR, over the range
1530
+ the BOARD recorded rather than two branch names — `links.tsx::cardRange`: one
1531
+ recorded commit goes through `commit/<sha>`, which the door diffs against its
1532
+ first parent, and several become `compare/<first>^...<last>`, whose base is the
1533
+ oldest commit's PARENT so that commit's own changes are inside the range. Both
1534
+ ends are durable, which is what makes the pane render for a card whose `tk-*`
1535
+ and `ms/*` branches were pruned before this host became the remote — the head
1536
+ alone was not enough, 0.5.2 shipped that and the pane stayed empty. Then the
1537
+ file list from `stat`, each file's patch on expand via `?path=`) and the fold on
1538
+ every commit row. Availability is
1531
1539
  DISCOVERED, not configured: the first refusal whose words are
1532
1540
  `gitdoor.py::NO_REPO` flips a module-level flag for the session and nothing asks
1533
1541
  again — an unknown ref does not flip it, because that means "ask again for
@@ -1541,6 +1549,10 @@ effect firing, and that half is covered against a real server in
1541
1549
  chapter): `pages/WorktreeDiff.tsx`, the full-width page a Worktrees row opens,
1542
1550
  hands its whole range `<milestone branch>...tk-<id>` to the very same
1543
1551
  `FilesChanged` — plus a summary bar, which is a prop on it and not a second ask.
1552
+ Two live BRANCHES there, deliberately, where the dossier asks by sha: that page's
1553
+ subject is a working tree that exists on disk right now, so where its two tips
1554
+ stand IS the question. `FilesChanged` therefore takes a whole `GitTarget` rather
1555
+ than a base/head pair — a pair could only ever express the second question.
1544
1556
  Nothing else moved to do that: **no verb, no stored key, no change to the door**,
1545
1557
  and no component fetches a patch outside `cascade()`. The one seam this cost is
1546
1558
  `FileList` exported beside `FilesChanged` — the pure half, given a step — for
@@ -1722,6 +1734,14 @@ A repo joined to nothing behaves exactly as it did.
1722
1734
 
1723
1735
  ### The hosted window: the host may hold a MIRROR of the declared forge (decided 2026-08-30)
1724
1736
 
1737
+ *Reversed one day later — "The host becomes the remote", below. The direction
1738
+ of the git flow inverts: the host holds the board's OWN repo (`repo.git`),
1739
+ takes pushes from enrolled principals, and mirrors OUTWARD to the forge;
1740
+ `mirror.git` retires. This section stays as the argument it was, because what
1741
+ it got right (a hosted window must stand on a real history; the switch is
1742
+ decided at mount time; the credential is a deploy key, never a token)
1743
+ survives the reversal, and what it got wrong is named there.*
1744
+
1725
1745
  The first amendment narrowed "no server reads a repo" to "a host that sits in a
1726
1746
  repo may read it". This one narrows the other clause — *the server deliberately
1727
1747
  has no clone* — and, as before, the change is recorded here rather than quietly
@@ -1918,6 +1938,251 @@ sh smoke.sh # taskops.bernardocastro.dev, task
1918
1938
  sh smoke.sh <host> <board> <private-board>
1919
1939
  ```
1920
1940
 
1941
+ ### The host becomes the remote (decided 2026-08-31 — reverses the mirror)
1942
+
1943
+ This section has been narrowed twice and each time the change was recorded
1944
+ rather than quietly made. This amendment is larger: it REVERSES a ban rather
1945
+ than narrowing one, and the words it reverses are one day old. "The mirror
1946
+ takes no client pushes — the host still exposes no receive door of any kind"
1947
+ stops being true. From this chapter on:
1948
+
1949
+ > **The board's host holds the board's git, as a real remote. A checkout's
1950
+ > `origin` IS the host: `git push` lands there, `git clone`/`fetch` reads from
1951
+ > there, and the declared forge becomes an OUTBOUND mirror the host pushes to,
1952
+ > best effort. The host never prunes a card branch.**
1953
+
1954
+ **Be fair to the ban before reversing it.** "Client pushes to the host" was
1955
+ banned when the host had no git at all — a directory of boards, `subprocess`
1956
+ nowhere near it — and the failure it guarded against was real: two clones
1957
+ replicating git state between themselves is split-brain with a merge driver,
1958
+ and a host that accepted pushes with no identity story would have been an
1959
+ anonymous write with a packfile attached. Both hazards are still hazards.
1960
+ What changed is not the hazards; it is that the host now HAS an identity
1961
+ story every door already checks (§19), and that the topology proposed here is
1962
+ not replication between clones at all — it is a hub, one direction on each
1963
+ leg. Writing the ban down was right. What the day-old design got wrong is
1964
+ narrower and worth naming exactly.
1965
+
1966
+ **What the pull-mirror got wrong.** The mirror's one source was the forge, so
1967
+ the board's view of the code became a FUNCTION OF SOMEBODY ELSE'S BRANCH
1968
+ HYGIENE — three failures, all observed live against the hosted window on
1969
+ 2026-08-31, the day after it shipped:
1970
+
1971
+ - a worktree branch that was never pushed **does not exist** for the host, so
1972
+ a card being worked on had no diff at its URL until its close pushed it;
1973
+ - a `tk-*` branch pruned on the forge when its chapter landed **disappeared
1974
+ from the board**, even though the work is intact and reachable from master.
1975
+ Verified against production: `git ls-remote origin` had neither `tk-dfaff7`
1976
+ nor `ms/parallel-by-mechanism-the-board-`, yet both cards' work is in the
1977
+ trunk. The stale sentence called that "normal, not a fault" — true of the
1978
+ MIRROR, and exactly the fault of the DESIGN: a landed card's diff is the
1979
+ board's own record and it had been made erasable by a forge-side `git push
1980
+ --delete` the board never sees;
1981
+ - and structurally, the host's git was a copy of a copy: worktree → GitHub →
1982
+ mirror, with GitHub — the viewer — sitting in the middle of the board's own
1983
+ data path.
1984
+
1985
+ The original ask, restated by Berna the same day, was the other topology all
1986
+ along: the taskops server keeps the history and mirrors it ONWARD to GitHub.
1987
+ The cost of adopting it now is small by construction: `gitwork/bind.py`
1988
+ already pushes `origin <tk-…>` on post-commit, best effort, never a gate — so
1989
+ for a joined checkout the only thing that changes is WHICH remote `origin`
1990
+ is. The lifecycle pushes in `gitwork/remote.py::push` keep their contract and
1991
+ their ten seconds; they just stop travelling through a third party.
1992
+
1993
+ The decisions the rest of the chapter follows, each argued:
1994
+
1995
+ **Transport: git smart-HTTP over the existing door, not ssh.** The relay in
1996
+ front of this host (§17) exposes no port but HTTPS, so ssh transport would
1997
+ begin by re-plumbing the deployment. It would also mean a SECOND auth system
1998
+ — an sshd, an `authorized_keys`, a key-to-principal mapping — beside the one
1999
+ the board already has, for keys that are already enrolled on the host and
2000
+ already end in a bearer session every door checks (§19: one introduction, one
2001
+ credential). Git speaks smart-HTTP natively (`info/refs?service=…`, then
2002
+ `git-upload-pack` / `git-receive-pack` POSTs), authenticates it with HTTP
2003
+ Basic, and a session token rides in that password field with no client-side
2004
+ tooling at all. ssh becomes worth revisiting only if BOTH of these turn true:
2005
+ a deployment that fronts raw TCP (no relay), and clients that cannot hold a
2006
+ session token. Neither exists, and the second is unlikely ever to — `taskops
2007
+ join` mints the session that signs everything else.
2008
+
2009
+ **The address: `/<board>/repo.git`.** A git client is pointed at
2010
+ `https://<host>/<board>/repo.git` and everything git asks for lives under it:
2011
+ `/<board>/repo.git/info/refs?service=git-upload-pack`,
2012
+ `/<board>/repo.git/git-upload-pack`, `/<board>/repo.git/git-receive-pack`.
2013
+ It cannot collide with anything the router already serves, and each half of
2014
+ that is a fact about an existing closed set, not a hope: the JSON diff door
2015
+ is the tail `git/…` (and `api/git/…` — `http/routes.py::split` strips the
2016
+ prefix once, and `repo.git` does not start with `api/` so the strip never
2017
+ touches it), while `repo.git` is its own first segment; and the page's assets
2018
+ are `static.py::asset`'s closed set — flat filenames whose suffix is in
2019
+ `TYPES`, and `.git` is not in `TYPES`, so no bundle that ever ships can
2020
+ shadow the repo and the repo can never shadow an asset. The `.git` suffix is
2021
+ also the convention every forge trained every tool on, which is why the name
2022
+ is not `code` or `src`: an address a person guesses should be the one that
2023
+ works.
2024
+
2025
+ **On-disk: a NEW bare repo, `<root>/<board>/repo.git`, and `mirror.git`
2026
+ retires.** The mirror does not get promoted, for two reasons written into its
2027
+ own contract. It is DERIVED and disposable — "delete it and it re-clones" —
2028
+ and the board's real remote is the opposite thing: truth the forge does not
2029
+ necessarily hold, deletable by nobody. And its refspec is `--mirror`, whose
2030
+ fetch semantics are *make local match the forge, prunes included* — the exact
2031
+ behaviour this chapter exists to kill would be sitting in the repo's own
2032
+ config, one habitual `git fetch` away. A truth-holder configured to erase
2033
+ itself is not a truth-holder. So: a fresh `git init --bare repo.git` beside
2034
+ `events.jsonl`, and on a host that already has a populated `mirror.git`
2035
+ (production does, today), `repo.git` is SEEDED from it — one local
2036
+ `git clone --bare mirror.git repo.git`, on-disk and cheap — so no history the
2037
+ mirror held is lost, and the mirror-mode config dies with the old directory,
2038
+ which is then removed. Wired as `gitwork/bare.py::adopt` and called by
2039
+ `http/repos.py`, the first door that needs an answer: it is the ONE read path
2040
+ in the codebase allowed to write, argued there — a migration of a history the
2041
+ host already possesses is not a directory conjured by a stranger's question,
2042
+ and a board holding neither repo still leaves the disk untouched. `mirror.py`
2043
+ is DELETED in the same card rather than left as a second source; a fallback
2044
+ that could only ever answer for refs the FORGE has is wrong in exactly the case
2045
+ the reader asks about (a branch nobody pushed, a branch the forge pruned), and
2046
+ `http/repos.py` carries that argument. `http/repos.py` points `/git` and the
2047
+ hosted window at `repo.git` from then on: the window's diffs are read from the board's OWN
2048
+ repository, with no dependency on the forge — which is the acceptance the
2049
+ chapter is held to.
2050
+
2051
+ **Roles: a push is a write, and §11's anonymous-write ban covers it.** Only
2052
+ an enrolled principal may push — `dev:<name>` and `agent:<dev>/<name>` alike,
2053
+ because workers are exactly who `bind.py` pushes as, on every commit. An
2054
+ anonymous caller and a read-only credential are refused at the receive door
2055
+ with the sentence that names `taskops join`. A read — clone, fetch,
2056
+ `ls-remote` — follows the board's VISIBILITY exactly as `/rpc` does: a public
2057
+ board clones anonymously (and, per §11's invisible-write ban, that read
2058
+ leaves no `presence` row), a private board clones with the credential the
2059
+ doors already check. One rule, already written, now asked by one more door.
2060
+
2061
+ **The host never prunes, and never takes a force.** A card branch pushed to
2062
+ the host is readable at its URL forever — that permanence is the point of the
2063
+ reversal, so it is enforced, not hoped: the receive door refuses a ref
2064
+ deletion and a non-fast-forward push outright. There is no flag that opens
2065
+ either, for `board rm`'s reason (§11): `--force` is a word that names no
2066
+ consequence, and what it would destroy here is the diff of a landed card —
2067
+ the board's own record. History rewriting, when it is ever needed, is the
2068
+ owner's deliberate act against the host's filesystem, not a verb.
2069
+
2070
+ **The outbound mirror: best effort, never a gate, failure visible.** The
2071
+ forge relationship inverts, it does not end: a board that declared one
2072
+ (`taskops board forge <owner>/<repo>`) gets its history pushed ONWARD, from
2073
+ `repo.git` to the forge, at the same lifecycle moments `remote.py::push`
2074
+ already owns — and a forge that is down MUST NOT fail the client's push,
2075
+ which lands on the host and is done. Best effort never means silent: a mirror
2076
+ push that fails is a FACT a reader can see — surfaced where the board already
2077
+ surfaces derived facts, on the payload, never swallowed into a log line —
2078
+ because an invisible best-effort is how the forge quietly drifts a month
2079
+ behind. The credential is an ssh deploy key on the host's filesystem with
2080
+ WRITE on the forge — and that is a real escalation over the mirror's
2081
+ read-only key, so it is named as one: minted and installed by the OWNER, an
2082
+ explicit act, revocable on the forge like any deploy key, and scoped to that
2083
+ one repo. §19.2 carries the credential argument. What it is NOT: a GitHub
2084
+ token (the key opens one repo, not an account), and never anything a dev
2085
+ supplies.
2086
+
2087
+ Wired as `gitwork/onward.py`, started by the receive door once git's own exit
2088
+ code says the push landed (`http/gitpack.py`) and running on a background
2089
+ thread — best effort in TIME as well as in outcome, since inline the client
2090
+ would feel the mirror as a ten-second gate on a push that already succeeded.
2091
+ Two pushes racing need no lock: the refspec is the whole `refs/heads/*`, so a
2092
+ later push subsumes an earlier one; only the REPORT is ordered, by timestamp
2093
+ (`store/mirroring.py`). The report is a row in `live.sqlite` — alive, never an
2094
+ event, because "could this host reach GitHub" is false the moment the log
2095
+ travels — and it rides on the board payload as `mirror` (`verbs/pulse.py`,
2096
+ under `forge`'s own contract: nothing to say sends no key), drawn as one
2097
+ MIRROR line by `mcp/boardview.py`, success included, so "silence" can never
2098
+ mean both "up to date" and "the key expired a month ago". The outbound remote
2099
+ is one named `forge` inside `repo.git`, added by the owner by hand — the ssh
2100
+ address IS the credential story, and a forge declared with no remote reads as
2101
+ a failure naming that exact command.
2102
+
2103
+ **A board with NO declared forge gets everything except the outbound
2104
+ mirror — and that is not a fault.** Push, clone, the hosted window's diffs,
2105
+ the permanence: all of it stands on `repo.git` alone. The forge was the
2106
+ SOURCE under the pull-mirror, so a board without one had no hosted window at
2107
+ all; under this topology the forge is a projection, and a board that wants no
2108
+ projection is simply complete. `NO_FORGE`'s refusal retires with the mirror
2109
+ it described — into `gitdoor.NO_REPO`, which now says the true board-level
2110
+ fact ("nobody has pushed this board's code here yet") and names the two moves
2111
+ that end it. The HOSTED WINDOW's gate moves the same way: `repos.backed` asks
2112
+ for `repo.git`, not for a forge, so `static.NO_UI` stops promising a page in
2113
+ exchange for `taskops board forge` — and the missing-ref sentence's AUDIENCE
2114
+ split (tk-9cde88) outlives the mirror that prompted it, the host's half
2115
+ rewritten as `stale.HOSTED`: this host prunes nothing, so a ref absent here was
2116
+ never pushed here. The window's half is unchanged, byte for byte.
2117
+
2118
+ **Beside §20, not inside it.** `board push`/`board pull` move the BOARD — the
2119
+ event log, the one history nobody can regenerate — and this chapter moves the
2120
+ CODE. The two lifecycles share a host and nothing else: `board push` still
2121
+ promotes a history exactly once onto an empty target, `board pull` is still a
2122
+ snapshot, `board rm` still guards on possession — and `board rm` now names
2123
+ one more thing it destroys, the board's `repo.git`, judged by the same
2124
+ possession logic: a history this checkout holds (the clone IS possession of
2125
+ the git half) is deletable, one it does not hold is refused.
2126
+
2127
+ **What stays banned, spelled out, because a reversal invites the question:**
2128
+
2129
+ - **replication BETWEEN clones.** This is a hub — worktree → host, host →
2130
+ forge, one direction on each leg. No clone pushes to another clone, no
2131
+ clone pulls the board's state from anywhere but the host, and the host
2132
+ never pulls git from the forge again.
2133
+ - **a stored GitHub token, and GitHub as a second credential type.** §19,
2134
+ unmoved. The outbound key is a deploy key: one repo, revocable, the
2135
+ owner's.
2136
+ - **anonymous writes, in any form — a git push included.** The receive door
2137
+ is behind the same credential wall as every write verb.
2138
+ - **a hand-rolled git protocol.** The pack protocol is git's, spoken by
2139
+ git's own plumbing through the ONE subprocess module (`gitwork/run.py`),
2140
+ the way SSHSIG verification is OpenSSH's (§11). A Python reimplementation
2141
+ of pkt-line is the crypto own-goal with a different file extension.
2142
+ - **a `--force` or a prune that could erase a card branch on the host.**
2143
+ Argued above; it is the reversal's own reason turned into a rule.
2144
+
2145
+ ### The dev's side: `taskops remote git`, and no `origin` (2026-08-31)
2146
+
2147
+ The reversal gave the host a git door and left the client unable to NAME it:
2148
+ `https://<host>/<board>/repo.git` existed only inside `http/gitpack.py`, so
2149
+ pointing a checkout at it meant reading the source. `taskops remote git` is
2150
+ that sentence — printed to paste, or `--add` to write it here — and it is a
2151
+ third action on `taskops remote` rather than a twelfth top-level command,
2152
+ because recording where the board's git lives is the same act as recording
2153
+ where the board lives (`cli/gitremote.py`).
2154
+
2155
+ **`origin` is never written, and `--name origin` is refused outright.** Not
2156
+ "left alone if it exists": refused. A checkout's `origin` is somebody's own
2157
+ setup — very often their GitHub — and no state of it is this command's to
2158
+ decide, its absence included. The remote is `taskops`; a name already in use
2159
+ is a refusal that names `--name <other>`, never a `set-url`. Adding a remote
2160
+ is CONNECTING (the CLI's promise); repointing one is managing.
2161
+
2162
+ **The credential is a HELPER, never a URL.** `https://x:<token>@host/…` is the
2163
+ obvious spelling and it is banned here for two reasons, either sufficient:
2164
+ `git remote add` persists its URL in `.git/config`, plaintext, so a live
2165
+ session token would sit in the one file nobody thinks to look at; and a
2166
+ taskops session EXPIRES, so a token baked into a config is wrong within the
2167
+ hour and cannot renew itself. What gets configured is
2168
+ `credential.<host>.helper`, pointing at `taskops hook credential` — git asks
2169
+ for a password at push time, the helper mints or renews a session from the ssh
2170
+ key already on disk (`identity.establish`, the same entry every board verb
2171
+ uses) and hands it over on a pipe. `store` and `erase` are answered with
2172
+ silence: a helper that caches is a helper that persists a token, and minting is
2173
+ free. One credential story, §19's, reaching git through git's own door — and
2174
+ `allowed_signers` remains the only thing the host keeps.
2175
+
2176
+ **The address is NOT a board-payload field, and that is the argued half.** The
2177
+ payload carries the forge and the mirror fact, so `repo_url` looks like it
2178
+ belongs beside them. It does not: the server cannot know its own public
2179
+ address — a proxy, a port-forward, the local `taskops ui` window, and every
2180
+ reader already reached it at an address the server never saw — so a stored one
2181
+ is the first field to rot and the first to send a dev to the wrong host. The
2182
+ address is (the host you asked) + `/<board>/repo.git`, a derivation the CLIENT
2183
+ can always do correctly and the server never can. `board ls` prints that shape
2184
+ once, and names the command that spells it exactly.
2185
+
1921
2186
  ---
1922
2187
 
1923
2188
  ## 17. Deployed — taskops.bernardocastro.dev is v2 (2026-08-08)
@@ -2473,6 +2738,36 @@ which `as_object` flattens to `{}`. A page of people silently reading as nobody
2473
2738
  is the bug that shape prevents, so the decoding belongs to whoever knows what it
2474
2739
  asked for and only the status and `Unreachable` are shared.
2475
2740
 
2741
+ ### 19.2 The mirror's key gains WRITE — the owner's explicit act (2026-08-31)
2742
+
2743
+ The hosted-window chapter (§16, 2026-08-30) added exactly one credential to
2744
+ this story: an ssh deploy key on the host's filesystem, READ-ONLY, so the
2745
+ host could pull a private repo's mirror. The reversal (§16, "The host becomes
2746
+ the remote") inverts the flow, and the key follows: the host now PUSHES to
2747
+ the declared forge, so the deploy key needs write on that one repo. That is a
2748
+ real escalation and it is treated as one, not smuggled in as a config detail:
2749
+
2750
+ - **It is the owner's explicit act, twice over.** Declaring the forge was
2751
+ already the owner's move (`taskops board forge`); minting a WRITE deploy
2752
+ key and installing it on the host is a second, separate decision the owner
2753
+ makes with eyes open, because it hands the host the power to move branches
2754
+ on the forge. Nothing mints it, discovers it, or asks a dev for it.
2755
+ - **It is still not a token, and the difference still matters.** A deploy key
2756
+ opens ONE repo; a GitHub token opens an account. If the host is ever
2757
+ compromised, the blast radius is the mirror of a repo whose real history
2758
+ the host itself holds — revoke the key on the forge and the leg is dead,
2759
+ the board untouched. Everything §19 bans stays banned: no stored GitHub
2760
+ token, no dev credential travelling, no second identity system.
2761
+ - **Write on the forge, never force.** The outbound push is the same
2762
+ best-effort, fast-forward-only discipline the host demands of its own
2763
+ receive door: the mirror leg cannot delete or rewrite on the forge either,
2764
+ so a key with write is not a key that erases.
2765
+
2766
+ A board whose owner declines the escalation keeps the read side whole: the
2767
+ host serves clones, pushes, and the hosted window from `repo.git`, and the
2768
+ forge simply goes unmirrored — visible as the mirror-push fact the payload
2769
+ carries, never as a fault.
2770
+
2476
2771
  ## 20. A board's whole life — create → push → (live) → pull → rm (2026-08-10)
2477
2772
 
2478
2773
  Until this chapter a board's life ran one way. `board create` made one, `board
@@ -3,6 +3,83 @@
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.3 — the Files-changed pane renders for a landed card
7
+
8
+ - **The pane asked for two branch names, and a landed card has neither.**
9
+ 0.5.2 made the HEAD durable — the card's newest recorded sha instead of its
10
+ branch — and stopped there. The BASE stayed the chapter's branch name, and
11
+ for every card that landed before this host became the board's remote that
12
+ branch was pruned on the forge, so the compare refused and the pane was
13
+ empty while the commits sat in the trunk. Reported live on tk-bffa26.
14
+ - **The range now comes from the card's own commits, both ends**
15
+ (`ui/src/links.tsx::cardRange`): one recorded commit goes through the commit
16
+ door, which already diffs a commit against its first parent; several become
17
+ `compare/<oldest>^...<newest>`. The base is the oldest commit's PARENT, not
18
+ the oldest commit — `git diff` is exclusive of its base, so `<oldest>...<newest>`
19
+ answers 200 with that commit's own changes silently missing, which is a pane
20
+ that looks complete and is not. No server change was needed: `<sha>^` already
21
+ passed the ref shape and resolves through `^{commit}`.
22
+ - **Pinned where it broke.** The pane renders the route it asked for, and a
23
+ smoke section reads that route off the rendered component — the previous
24
+ attempt was pinned one layer below the call site, so a call-site mutation
25
+ went green while the pane stayed broken. `smoke.sh` now derives the same
26
+ route from the LIVE card payload and asserts the first commit's files are in
27
+ the answer.
28
+ - **"so them was never pushed here"** — the host's missing-ref refusal had a
29
+ plural substitution in a subject slot. The number now runs through the whole
30
+ sentence, and both forms are pinned as bytes.
31
+
32
+ ## 0.5.2 — the host holds the git; GitHub is the copy
33
+
34
+ 0.5.0 and 0.5.1 built the hosted window on a mirror the host PULLED from the
35
+ declared forge. That was the wrong direction, and the reader felt it: a
36
+ worktree branch nobody had pushed did not exist for the host, and a `tk-*`
37
+ branch pruned when its chapter landed erased a finished card's diff even
38
+ though the commits were sitting in the trunk. The board's view of the code was
39
+ a function of somebody else's branch hygiene. This release inverts it.
40
+
41
+ - **The host IS the board's git remote.** `<root>/<board>/repo.git`, served
42
+ over smart-HTTP at `https://<host>/<board>/repo.git`: clone, fetch and push.
43
+ A worktree's card branch reaches it at the moment of the commit — the
44
+ post-commit hook already pushed `origin <tk-…>`, so for a joined checkout the
45
+ only thing that changes is which remote that is.
46
+ - **It never prunes.** `receive.denyDeletes` and `receive.denyNonFastForwards`
47
+ live in the repo's own config, so the refusal comes from the very
48
+ `receive-pack` process that would move the ref: no window between an
49
+ application check and the update, no hook script a later `git init` can drop,
50
+ and a client's `--force` changes nothing. A landed card's diff is readable at
51
+ its URL forever.
52
+ - **GitHub becomes the outbound copy.** After a push lands, the host pushes
53
+ onward to the declared forge on a background thread — best effort in outcome
54
+ AND in time, since inline a hanging forge would make the mirror felt as a
55
+ ten-second gate on a push that already succeeded. Fast-forward only: no
56
+ `--mirror` (it deletes), no `--force`, no `--prune`. The host does not do to
57
+ the forge what it refuses for itself.
58
+ - **A mirror failure is never silent.** Success and failure both land on the
59
+ board payload as `mirror` and on the orchestrator's board view — "up to date,
60
+ pushed 2m ago" or "FAILED just now: <git's own words>" — kept in
61
+ `live.sqlite` rather than the event log, because "this host reached GitHub ten
62
+ seconds ago" replayed on another machine is a lie.
63
+ - **One credential, still.** A push authenticates as the enrolled principal the
64
+ board already knows: git's HTTP Basic carries a session in the password
65
+ field, minted at push time from the dev's ssh key by a credential helper
66
+ (`taskops hook credential`), so no token is ever written into `.git/config`.
67
+ The receive door answers 401 with `WWW-Authenticate: Basic`, because git only
68
+ volunteers a credential after a challenge. Reads follow the board's
69
+ visibility exactly as `/rpc` does.
70
+ - **`taskops remote git [--add]`** wires a checkout to the board's git and
71
+ prints what it did. It refuses `--name origin` outright: repointing somebody's
72
+ own remote is not connecting, it is rewriting. `taskops join` does not do it
73
+ for you — it is the one act that could silently change where a push goes.
74
+ - **The pull mirror is deleted**, not kept as a fallback: it could only ever
75
+ answer for refs the forge has, which is wrong in exactly the case a reader
76
+ asks about. A host that already holds a populated `mirror.git` migrates it
77
+ once — `git clone --bare` into `repo.git`, then the mirror is removed.
78
+ - **The owner's two acts**, if they want GitHub kept in sync: a WRITE deploy key
79
+ on the host, and `git -C <root>/<board>/repo.git remote add forge
80
+ git@github.com:<owner>/<repo>.git`. Until then every board read says the
81
+ mirror is not configured. Still no stored GitHub token, anywhere.
82
+
6
83
  ## 0.5.1 — the hosted window, actually usable
7
84
 
8
85
  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