taskops-cli 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ARCHITECTURE.md +106 -11
  2. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/CHANGELOG.md +77 -0
  3. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/PKG-INFO +7 -5
  4. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/README.md +6 -4
  5. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_version.py +1 -1
  6. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/serving.py +9 -3
  7. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/types.py +12 -0
  8. taskops_cli-0.5.0/src/taskops/gitwork/mirror.py +132 -0
  9. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/gitdoor.py +29 -48
  10. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/handler.py +28 -7
  11. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/mounts.py +11 -12
  12. taskops_cli-0.5.0/src/taskops/http/repos.py +98 -0
  13. taskops_cli-0.5.0/src/taskops/http/stale.py +62 -0
  14. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/static.py +30 -15
  15. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/upstream.py +59 -7
  16. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/schema.py +1 -1
  17. taskops_cli-0.5.0/src/taskops/ui/app.js +61 -0
  18. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_facts.py +4 -4
  19. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/activity.py +7 -1
  20. taskops_cli-0.5.0/tests/test_mirror.py +164 -0
  21. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_topology.py +277 -6
  22. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_verbs.py +29 -1
  23. taskops_cli-0.5.0/ui/smoke/sections/all-chapters-scope.tsx +99 -0
  24. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/git-diff.tsx +6 -2
  25. taskops_cli-0.5.0/ui/smoke/sections/hosted-window.tsx +96 -0
  26. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/App.tsx +24 -2
  27. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Patch.tsx +1 -1
  28. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/MilestonePicker.tsx +28 -9
  29. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/links.tsx +5 -3
  30. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Reports.tsx +6 -5
  31. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/useBoard.ts +6 -3
  32. taskops_cli-0.4.1/src/taskops/ui/app.js +0 -61
  33. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.claude/settings.json +0 -0
  34. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.gitattributes +0 -0
  35. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  36. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.github/workflows/workflow.yml +0 -0
  37. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.gitignore +0 -0
  38. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.mcp.json +0 -0
  39. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/cache.sqlite +0 -0
  40. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/cache.sqlite-shm +0 -0
  41. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/cache.sqlite-wal +0 -0
  42. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/events.jsonl +0 -0
  43. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/live.sqlite +0 -0
  44. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/live.sqlite-shm +0 -0
  45. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/live.sqlite-wal +0 -0
  46. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board.json +0 -0
  47. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/reports/lifecycle-chapter.html +0 -0
  48. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/reports/reports-chapter.html +0 -0
  49. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/ui.lock +0 -0
  50. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/CLAUDE.md +0 -0
  51. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/pyproject.toml +0 -0
  52. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/__init__.py +0 -0
  53. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_clock.py +0 -0
  54. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_errors.py +0 -0
  55. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_ids.py +0 -0
  56. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_json.py +0 -0
  57. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_locate.py +0 -0
  58. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_wire.py +0 -0
  59. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/board.py +0 -0
  60. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/__init__.py +0 -0
  61. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/__main__.py +0 -0
  62. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/admin.py +0 -0
  63. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/claude.py +0 -0
  64. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/commands.py +0 -0
  65. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/enrol.py +0 -0
  66. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/github.py +0 -0
  67. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/grants.py +0 -0
  68. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/hooks.py +0 -0
  69. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/main.py +0 -0
  70. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/operate.py +0 -0
  71. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/paging.py +0 -0
  72. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/parser.py +0 -0
  73. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/pull.py +0 -0
  74. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/push.py +0 -0
  75. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/remote.py +0 -0
  76. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/rm.py +0 -0
  77. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/team.py +0 -0
  78. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/watch.py +0 -0
  79. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/window.py +0 -0
  80. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/wording.py +0 -0
  81. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/__init__.py +0 -0
  82. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/actors.py +0 -0
  83. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/challenge.py +0 -0
  84. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/chapters.py +0 -0
  85. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/event.py +0 -0
  86. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/forge.py +0 -0
  87. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/graph.py +0 -0
  88. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/holding.py +0 -0
  89. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/hours.py +0 -0
  90. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/kinds.py +0 -0
  91. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/machine.py +0 -0
  92. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/mentions.py +0 -0
  93. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/replay.py +0 -0
  94. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/reports.py +0 -0
  95. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/review.py +0 -0
  96. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/scope.py +0 -0
  97. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/seams.py +0 -0
  98. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/__init__.py +0 -0
  99. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/bind.py +0 -0
  100. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/catchup.py +0 -0
  101. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/claudefiles.py +0 -0
  102. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/diff.py +0 -0
  103. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/install.py +0 -0
  104. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/landing.py +0 -0
  105. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/patch.py +0 -0
  106. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/remote.py +0 -0
  107. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/run.py +0 -0
  108. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/sig.py +0 -0
  109. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/trailer.py +0 -0
  110. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/trees.py +0 -0
  111. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/__init__.py +0 -0
  112. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/admin.py +0 -0
  113. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/auth.py +0 -0
  114. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/feed.py +0 -0
  115. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/grants.py +0 -0
  116. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/ingest.py +0 -0
  117. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/login.py +0 -0
  118. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/members.py +0 -0
  119. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/removal.py +0 -0
  120. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/rpc.py +0 -0
  121. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/scoped.py +0 -0
  122. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/server.py +0 -0
  123. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/watcher.py +0 -0
  124. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/identity.py +0 -0
  125. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/__init__.py +0 -0
  126. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/__main__.py +0 -0
  127. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/activity.py +0 -0
  128. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/before.py +0 -0
  129. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/boards.py +0 -0
  130. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/boardview.py +0 -0
  131. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/brief.py +0 -0
  132. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/chapter.py +0 -0
  133. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/dossier.py +0 -0
  134. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/fields.py +0 -0
  135. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/gitmoves.py +0 -0
  136. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/hello.py +0 -0
  137. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/integrate.py +0 -0
  138. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/orders.py +0 -0
  139. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/render.py +0 -0
  140. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/server.py +0 -0
  141. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/thread.py +0 -0
  142. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/tools.py +0 -0
  143. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/py.typed +0 -0
  144. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/session.py +0 -0
  145. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/__init__.py +0 -0
  146. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/cache.py +0 -0
  147. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/creds.py +0 -0
  148. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/handover.py +0 -0
  149. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/live.py +0 -0
  150. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/log.py +0 -0
  151. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/pubkeys.py +0 -0
  152. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/reviews.py +0 -0
  153. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/server.py +0 -0
  154. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/stores.py +0 -0
  155. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/ui/index.html +0 -0
  156. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/ui/style.css +0 -0
  157. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/__init__.py +0 -0
  158. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_args.py +0 -0
  159. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_cards.py +0 -0
  160. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_chapter.py +0 -0
  161. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_context.py +0 -0
  162. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_mentions.py +0 -0
  163. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_rows.py +0 -0
  164. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_stories.py +0 -0
  165. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_waiting.py +0 -0
  166. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_windows.py +0 -0
  167. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/assign.py +0 -0
  168. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/card.py +0 -0
  169. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/events.py +0 -0
  170. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/filed.py +0 -0
  171. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/plan.py +0 -0
  172. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/project.py +0 -0
  173. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/pulse.py +0 -0
  174. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/record.py +0 -0
  175. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/report.py +0 -0
  176. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/review.py +0 -0
  177. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/take.py +0 -0
  178. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/update.py +0 -0
  179. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/__init__.py +0 -0
  180. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/conftest.py +0 -0
  181. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_architecture.py +0 -0
  182. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_claude.py +0 -0
  183. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_core.py +0 -0
  184. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_git.py +0 -0
  185. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_mcp.py +0 -0
  186. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_report.py +0 -0
  187. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_store.py +0 -0
  188. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_ui.py +0 -0
  189. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_window.py +0 -0
  190. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/ui_harness.js +0 -0
  191. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/build.mjs +0 -0
  192. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/index.html +0 -0
  193. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/package-lock.json +0 -0
  194. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/package.json +0 -0
  195. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/fixture.json +0 -0
  196. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/main.tsx +0 -0
  197. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/run.mjs +0 -0
  198. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/actors-window-filter.tsx +0 -0
  199. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/actors.tsx +0 -0
  200. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/board-columns.tsx +0 -0
  201. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/board-flow-view.tsx +0 -0
  202. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
  203. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
  204. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapter-landed.tsx +0 -0
  205. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
  206. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapters-open.tsx +0 -0
  207. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/comment-box.tsx +0 -0
  208. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
  209. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
  210. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/diff-page.tsx +0 -0
  211. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/dossier.tsx +0 -0
  212. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/event-stream.tsx +0 -0
  213. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/feed-reconnect.tsx +0 -0
  214. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/forge-links.tsx +0 -0
  215. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
  216. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/monitor-panes.tsx +0 -0
  217. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/overlay-escape.tsx +0 -0
  218. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/report-markdown.tsx +0 -0
  219. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/report-sandbox.tsx +0 -0
  220. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/reviewing-skew.tsx +0 -0
  221. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/section.ts +0 -0
  222. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/swarm-topology.tsx +0 -0
  223. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/thread-closing-note.tsx +0 -0
  224. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/worktrees-index.tsx +0 -0
  225. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections.mjs +0 -0
  226. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/client.ts +0 -0
  227. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/completed.ts +0 -0
  228. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/actors/Daysheet.tsx +0 -0
  229. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/actors/DevPanel.tsx +0 -0
  230. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/CardTile.tsx +0 -0
  231. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/Column.tsx +0 -0
  232. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/ViewToggle.tsx +0 -0
  233. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/flip.ts +0 -0
  234. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/useFlip.ts +0 -0
  235. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/CommentBox.tsx +0 -0
  236. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Drawer.tsx +0 -0
  237. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Sections.tsx +0 -0
  238. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Thread.tsx +0 -0
  239. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/split.ts +0 -0
  240. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/tokens.ts +0 -0
  241. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/AvatarStack.tsx +0 -0
  242. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/Header.tsx +0 -0
  243. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/KpiRail.tsx +0 -0
  244. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/TabNav.tsx +0 -0
  245. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/flow/FlowView.tsx +0 -0
  246. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/flow/layout.ts +0 -0
  247. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Chapter.tsx +0 -0
  248. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/DependencyChain.tsx +0 -0
  249. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/EditSurface.tsx +0 -0
  250. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/EventStream.tsx +0 -0
  251. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
  252. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/LiveLeases.tsx +0 -0
  253. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Mentions.tsx +0 -0
  254. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Pane.tsx +0 -0
  255. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Swarm.tsx +0 -0
  256. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Throughput.tsx +0 -0
  257. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/panels.ts +0 -0
  258. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/reports/ReportFrame.tsx +0 -0
  259. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Avatar.tsx +0 -0
  260. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Markdown.tsx +0 -0
  261. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Overlay.tsx +0 -0
  262. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Segmented.tsx +0 -0
  263. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/overlayStack.ts +0 -0
  264. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/story/ChapterStory.tsx +0 -0
  265. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/story/stats.ts +0 -0
  266. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/toasts/ToastStack.tsx +0 -0
  267. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/toasts/model.ts +0 -0
  268. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/toasts/useToasts.ts +0 -0
  269. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/format.ts +0 -0
  270. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/hoursWindow.ts +0 -0
  271. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/main.tsx +0 -0
  272. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/markdown.ts +0 -0
  273. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Actors.tsx +0 -0
  274. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Board.tsx +0 -0
  275. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Monitor.tsx +0 -0
  276. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/WorktreeDiff.tsx +0 -0
  277. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Worktrees.tsx +0 -0
  278. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/theme/theme.ts +0 -0
  279. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/theme/tokens.css +0 -0
  280. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/types.ts +0 -0
  281. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/useEvents.ts +0 -0
  282. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/styles/index.css +0 -0
  283. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/tsconfig.json +0 -0
  284. {taskops_cli-0.4.1 → taskops_cli-0.5.0}/uv.lock +0 -0
@@ -1012,7 +1012,8 @@ Both remaining items are done. The migration ran against all four v1 boards
1012
1012
  directory of board logs, so it is `pip install` into a venv and one pm2 entry
1013
1013
  (README, "Deployed"). **The box runs this tree** since 2026-08-09 (tk-df8e64,
1014
1014
  §17), so `https://taskops.bernardocastro.dev/<board>/ui/` answers `410` and one
1015
- sentence, exactly as the trunk does (§16, §17). Re-derivable:
1015
+ sentence, exactly as the trunk does (§16, §17) — until a board with a declared
1016
+ forge gets its hosted window (§16, "The hosted window"). Re-derivable:
1016
1017
  `curl -s https://taskops.bernardocastro.dev/healthz` and
1017
1018
  `curl -w '%{http_code}' https://taskops.bernardocastro.dev/axion/ui/`
1018
1019
  — measured `{"boards": 4}` (after all four boards had been addressed through
@@ -1361,7 +1362,17 @@ files that answer it:
1361
1362
  * **the card dossier drawer**, opening over Monitor and Board through `App`'s
1362
1363
  `openCard` — it renders the acceptance criteria no v1 screen ever drew, and
1363
1364
  carries the UI's ONE write, the comment box with its mention picker
1364
- . The header's milestone picker scopes every tab at once.
1365
+ . The header's milestone picker scopes every tab at once, and **"all
1366
+ chapters" is an ARGUMENT** — `milestone=*` (`core/types.py::EVERYTHING`), not
1367
+ the absence of one. It sent nothing for a wave, which the server reads as
1368
+ "resolve the scope yourself" and resolves to the single open chapter: the page
1369
+ arrived narrowed to that chapter with the ✓ sitting on "all chapters" the whole
1370
+ time, and clicking the option changed no argument (2026-08-18). Absence still
1371
+ means exactly that — it is the dashboard's OPENING state, and `App::scopeOf`
1372
+ resolves it against the chapter the answer came back scoped to, so the ✓ marks
1373
+ the scope the page is actually drawn at. `verbs/activity.py` refuses `*` in the
1374
+ words of its own doors: a story is one chapter's cards by definition.
1375
+ `ui/smoke/sections/all-chapters-scope.tsx`.
1365
1376
 
1366
1377
  * **the comment toasts**, a stack bottom-right over every tab
1367
1378
  (`ui/src/components/toasts/`), because the UI's one write was invisible to
@@ -1495,15 +1506,18 @@ construction and never sniffed per request** (`Mounts(repo=…)`,
1495
1506
  | `taskops serve` | a directory of boards, no checkout | **404 with the reason spelled out**, and nothing faked | **410 and one sentence** |
1496
1507
 
1497
1508
  The `/ui/` column arrived later, and it is the same `repo` deciding both — see
1498
- "API ONLY is now literal" below.
1509
+ "API ONLY is now literal" below. Both cells of the `serve` row are narrowed a
1510
+ second time by "The hosted window" below: a board that DECLARED a forge may be
1511
+ answered from a bare read-only mirror of it, and only then.
1499
1512
 
1500
1513
  A viewer meets that gap whenever this clone cannot answer — a ref not fetched
1501
1514
  yet, a shallow checkout, a commit from before this repo had the branch — and
1502
1515
  there the UI's declared cascade takes over: numstat from the event → patch from
1503
1516
  /git → the forge link if a slug exists → one honest sentence. No dead anchor, no
1504
1517
  empty pane pretending. (The gap used to have a second, worse shape: a browser
1505
- with no clone at all, pointed at the hosted page. There is no hosted page any
1506
- moresee below.)
1518
+ with no clone at all, pointed at the hosted page. That page was withdrawn see
1519
+ belowand comes back only standing on a real history: the forge mirror, in
1520
+ "The hosted window".)
1507
1521
 
1508
1522
  That cascade is ONE function — `ui/src/links.tsx::cascade`, beside the slug and
1509
1523
  the link templates it already owned — and `ui/src/components/card/Patch.tsx`
@@ -1585,9 +1599,11 @@ doubled separator and the directory itself, and never repairs one: a traversal
1585
1599
  normalised into something acceptable is the bug. Anything outside
1586
1600
  `.taskops/reports/` is a 400 that names the directory, and `tests/test_topology.py`
1587
1601
  walks every shape (a real secret one directory up included, asserting its
1588
- contents appear nowhere in the answer). `taskops serve` still mounts no `/git`
1589
- at all: the switch is `Mounts.repo`, decided once at construction, and a new
1590
- question underneath it opens no door.
1602
+ contents appear nowhere in the answer). `taskops serve` mounts no `/git`
1603
+ from a checkout it does not have: the switch is `Mounts.repo`, decided once at
1604
+ construction, and a new question underneath it opens no door. The hosted-window
1605
+ amendment below changes what `repo` may point AT — a bare mirror of the
1606
+ declared forge — never the switch itself.
1591
1607
 
1592
1608
  ### The window: `taskops ui` serves locally even for a REMOTE board (2026-08-08)
1593
1609
 
@@ -1626,7 +1642,7 @@ The routes do not change, so the committed bundle is untouched by any of this:
1626
1642
  decision). The server used to serve the bundle too, from a `--ui` flag with a
1627
1643
  packaged default — so the box above answered `/ui/` with a real dashboard, and
1628
1644
  that dashboard could not draw a single patch, because the machine it was served
1629
- from deliberately has no clone. It was a degraded window presented as *the*
1645
+ from deliberately had no clone. It was a degraded window presented as *the*
1630
1646
  window; the redirect above merely pointed people at it. **The binary serves the
1631
1647
  window; the server serves the truth.**
1632
1648
 
@@ -1634,7 +1650,9 @@ So: `Mounts.ui` is not a parameter, it is `repo`'s shadow — one construction-t
1634
1650
  switch mounts `/git` AND the bundle, and only a process standing in a checkout
1635
1651
  has either. `/ui/` on a board host answers `static.py::NO_UI` as plain text with
1636
1652
  **410 Gone**, not 404: 404 says "no idea, maybe later", and this page was
1637
- withdrawn on purpose and is never coming back to a host with no clone. Plain
1653
+ withdrawn on purpose and is never coming back to a host with no clone — and a
1654
+ host holding the board's forge MIRROR is no longer that host ("The hosted
1655
+ window", below). Plain
1638
1656
  text and not the JSON envelope because the only reader who reaches that door is
1639
1657
  a human with a browser. The `--ui` flag is REMOVED, not deprecated: there is no
1640
1658
  command line that puts the decision back. The bundle still ships inside the
@@ -1659,6 +1677,21 @@ local process**; the browser holds only the token `taskops ui` mints into
1659
1677
  `ui.json`, which is local and revocable. `tests/test_topology.py` searches every
1660
1678
  served byte, and their headers, for the team token.
1661
1679
 
1680
+ **A SESSION RUNS OUT AND THE WINDOW DOES NOT** (2026-08-18). A host session is
1681
+ twelve hours (`http/login.py::SESSION_TTL`), and a window is a thing you leave
1682
+ open: `_upstream` read the token lying in `remote.json` and `Upstream` held it
1683
+ for the life of the process, so the morning after, every read answered "that
1684
+ credential expired" — and the habit that taught was `taskops join`, a human
1685
+ re-pasting a credential the machine can mint itself, which is the exact thing
1686
+ the session chapter exists to end. Two halves, both fixed: the command opens
1687
+ with `session.fresh`, like every other command has since that chapter landed,
1688
+ and the window is handed `session.refresher` so it can sign in AGAIN mid-flight
1689
+ — `board.py::RemoteBoard.call`'s retry, narrow for the same reason (the server
1690
+ said `EXPIRED`, this process holds a key). A window with no way back in — a
1691
+ standing bearer, a public viewer — is unchanged: `refresh` is None and nobody
1692
+ replaces a token behind its owner's back (`session.py`). `Upstream` still knows
1693
+ nothing about keys or files: it is handed a callable.
1694
+
1662
1695
  **A refusal from the remote arrives in the SERVER's own words**, status and all:
1663
1696
  `upstream.py` returns `(status, bytes)` and re-wraps only a body that is not an
1664
1697
  envelope. A board that says "held by agent:berna/w1" with a 409 must not reach
@@ -1684,6 +1717,66 @@ move a branch under a worktree somebody is sitting in.
1684
1717
 
1685
1718
  A repo joined to nothing behaves exactly as it did.
1686
1719
 
1720
+ ### The hosted window: the host may hold a MIRROR of the declared forge (decided 2026-08-30)
1721
+
1722
+ The first amendment narrowed "no server reads a repo" to "a host that sits in a
1723
+ repo may read it". This one narrows the other clause — *the server deliberately
1724
+ has no clone* — and, as before, the change is recorded here rather than quietly
1725
+ made:
1726
+
1727
+ > **A board host MAY hold a bare, READ-ONLY mirror of the board's DECLARED
1728
+ > forge — and only then does it answer `/git` and `/ui/`.** A board with no
1729
+ > declared forge is exactly what it was: 404 on `/git`, 410 on `/ui/`, each
1730
+ > refusal naming `taskops board forge <owner>/<repo>` as the door.
1731
+
1732
+ Why this is not the degraded window "API ONLY" withdrew. That window's flaw was
1733
+ never that a server drew a dashboard — it was that the machine drawing it had
1734
+ NO git at all, so every diff fell through the whole cascade to a forge link or
1735
+ a sentence, a blank presented as *the* window. The mirror removes the flaw, not
1736
+ the caution: it is **the same history the forge already holds** — `git clone
1737
+ --mirror` of the repo the owner declared with `board forge`, refreshed by ONE
1738
+ bounded on-demand fetch when a requested ref is missing, then answered stale
1739
+ rather than blocking — so a diff served from it is the diff GitHub would show,
1740
+ read by the same `gitwork/diff.py` doors, behind the same visibility rules as
1741
+ `/rpc`. And the dev-side story does not move an inch: `taskops ui` still serves
1742
+ the window from the checkout it stands in, reading the dev's OWN clone,
1743
+ including refs the forge has never seen. The hosted window is for the reader
1744
+ who has no clone — the case the local window, by construction, cannot serve.
1745
+
1746
+ **Where it lives**: `<root>/<board>/mirror.git`, a bare repo beside the board's
1747
+ `events.jsonl` — later cards follow this wording. It is DERIVED and disposable
1748
+ in exactly the sense `cache.sqlite` is: delete it and it re-clones from the
1749
+ declared forge; nothing in it is truth the forge does not hold. Its ONLY source
1750
+ is that forge. It takes no client pushes — the host still exposes no receive
1751
+ door of any kind — and it is not a second trunk: nothing lands there, nothing
1752
+ is read back into the log from it, and `taskops_merge` still happens in the
1753
+ dev's shared checkout.
1754
+
1755
+ **The credential story** is §19's, unchanged in kind. A public repo needs no
1756
+ credential at all. A private repo is fetched with an **ssh deploy key on the
1757
+ host's filesystem** — read-only, minted and installed by the OWNER, revocable
1758
+ on the forge like any deploy key — which is the ONE new credential this chapter
1759
+ adds, and it is the owner's business exactly as `board forge` itself is. What
1760
+ remains banned is what §11 and §19 already ban: a stored GitHub token (the
1761
+ mirror's remote is an address, never a token), a write credential in any form,
1762
+ GitHub as a second identity system, and any dev credential travelling to the
1763
+ host — nobody's `gh auth token` is involved, because the owner already declared
1764
+ the repo and the mirror pulls from the outside like any anonymous (or
1765
+ deploy-keyed) clone.
1766
+
1767
+ **Still banned, spelled out**: client pushes to the host and git replication
1768
+ between clones (the mirror replicates FROM the forge, one direction, and no
1769
+ clone ever pushes to it); a write credential on the host; a second trunk; a
1770
+ `--ui` flag or any per-request sniffing (whether the mirror exists is decided
1771
+ at mount time, the same `Mounts.repo` switch, now pointable at `mirror.git`);
1772
+ and anonymous writes — a public board's hosted window is a READ, and it leaves
1773
+ no `presence` row.
1774
+
1775
+ So `static.py::NO_UI` and `gitdoor.py::NO_REPO` change meaning, not words of
1776
+ intent: each stops saying "this host never has a repo" and starts saying "this
1777
+ BOARD declared no forge, so this host holds no mirror to read from" — a state
1778
+ the owner ends with one `taskops board forge`. The refusal still names the door.
1779
+
1687
1780
  ### A concept named by two cards is a seam — the fan-out rule, third notch
1688
1781
 
1689
1782
  `gitwork/remote.py` was born TWICE in one wave: two workers, each over the
@@ -1778,7 +1871,9 @@ request (`http/mounts.py::stores`), so a just-restarted process says `1`, not
1778
1871
 
1779
1872
  **That md5-of-the-served-bundle check is history, not the procedure any more.**
1780
1873
  Since §16's "API ONLY" amendment the host serves no bundle: `/<board>/ui/`
1781
- answers `410` and one sentence, so there is nothing at the domain to md5. The
1874
+ answers `410` and one sentence (until the hosted-window chapter deploys a board
1875
+ whose forge is declared — §16, "The hosted window"), so there is nothing at the
1876
+ domain to md5. The
1782
1877
  wheel is still checked for its markers *before* it ships — it is the same wheel
1783
1878
  teammates install to get `taskops ui` — and the domain is verified by DATA,
1784
1879
  through `/rpc`.
@@ -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.0 — the hosted window
7
+
8
+ - **A board can be READ without a clone.** Until now the dashboard was only ever
9
+ served by `taskops ui`, a binary standing in a checkout: a host running
10
+ `taskops serve` answered `/git` with a 404 and `/ui/` with a 410, because a
11
+ dashboard draws DIFFS and the host deliberately had no repository to read them
12
+ from. Serving a page that could not draw a single patch was a degraded window
13
+ presented as *the* window, so it was withdrawn. That reasoning had one
14
+ unexamined premise — that the host can never have git — and this release
15
+ removes it, argued in ARCHITECTURE §16 ("The hosted window") before a line of
16
+ code moved.
17
+ - **The host may hold a bare, READ-ONLY mirror of the board's DECLARED forge**,
18
+ at `<root>/<board>/mirror.git`, and only then does it answer `/git` and
19
+ `/ui/`. The mirror is derived and disposable in exactly the sense
20
+ `cache.sqlite` is: delete it and it re-clones; nothing in it is truth the
21
+ forge does not already hold. Its ONLY source is the repo the owner declared
22
+ with `taskops board forge` — it takes no client pushes, it is not a second
23
+ trunk, and nothing is ever read back into the log from it.
24
+ - **A board that declared no forge is exactly what it was**: 404 on `/git`,
25
+ 410 on `/ui/`, and each refusal now names the door — `taskops board forge
26
+ <owner>/<repo>` — instead of stranding the reader in a sentence about a host
27
+ they cannot change.
28
+ - **No new credential type.** A public repo is mirrored anonymously; a private
29
+ one is fetched with an ssh deploy key on the host's filesystem, installed by
30
+ the OWNER, revocable on the forge — the one new credential this chapter adds,
31
+ and it is the owner's business exactly as `board forge` itself is. No stored
32
+ GitHub token, no dev credential travelling, no second identity system.
33
+ - **The page opens on the FACT, never on a clone.** `/ui/` is granted by the
34
+ declared forge alone: the bundle's bytes come from the installed package, so
35
+ putting a network clone in front of `index.html` would take a static page down
36
+ whenever the forge is unreachable. The mirror resolves lazily the moment a
37
+ diff is actually read — which is where a failure belongs, in the diff pane, in
38
+ the door's own words.
39
+ - **A missing ref buys exactly ONE bounded fetch**, then answers stale rather
40
+ than blocking a page on a forge that is down — and only on a mirror. A
41
+ window's own clone still fetches nothing on the reader's behalf: a background
42
+ fetch inside a read-only door would move a branch under a worktree somebody is
43
+ sitting in.
44
+ - **Reads stay reads.** `/ui/` sits behind the credential `/rpc` asks for —
45
+ a `public` board serves the page to an anonymous reader, a private one refuses
46
+ with the same join sentence — and an anonymous page load writes nothing at
47
+ all, not even the invisible write: no presence row.
48
+
49
+ ## 0.4.2 — the filter that filtered nothing, and the window that stopped signing in
50
+
51
+ - **"All chapters" is an ARGUMENT now, and it really shows every chapter.** The
52
+ option sent no `milestone=` at all — and an absent milestone never meant "the
53
+ whole board": it means "server, resolve the scope yourself", which resolves to
54
+ the single open chapter when there is exactly one. So on a board with one
55
+ chapter open and seven landed (Berna's, reported 2026-08-18) the page came back
56
+ narrowed to that chapter, the menu drew its ✓ beside a scope that had never
57
+ been in effect, and clicking the option changed no argument at all — the only
58
+ way to see other work was to name every chapter in turn. Board-wide is now
59
+ asked for by name (`milestone=*`, `core/types.py::EVERYTHING`). Absence still
60
+ means exactly what it meant, so no agent and no existing caller sees any
61
+ change: it is the dashboard's OPENING state, and the ✓ now falls on the chapter
62
+ the server actually resolved, so the pill can no longer claim a scope the page
63
+ is not drawn at. `taskops_activity` refuses `*` in the words of its own doors —
64
+ a story is one chapter's cards by definition — where it used to answer
65
+ `milestone * does not exist`, a lookup's answer to a question that was never a
66
+ lookup.
67
+ - **A window whose session ran out signs in again instead of teaching `taskops
68
+ join`.** A host session is twelve hours and a window is a thing you leave open.
69
+ Two halves of one hole, both on the `taskops ui` path and nowhere else: the
70
+ command read the token lying in `remote.json` instead of `session.fresh` —
71
+ which every other command has taken since sessions landed — so a window opened
72
+ on yesterday's session was dead on arrival; and the forwarder held the token it
73
+ was built with for the life of the process, so a window that was open when the
74
+ session expired answered "that credential expired" to every read. Neither path
75
+ could come back, so the habit it taught was re-joining: a human pasting a
76
+ credential the machine can mint itself from the key it already has. The retry
77
+ is `RemoteBoard.call`'s and just as narrow — the SERVER said EXPIRED, and this
78
+ process holds a key — and it is a callable passed in, so the forwarder still
79
+ knows nothing about keys, files or logins. A window with no way back in (a
80
+ standing bearer token, a public viewer) is unchanged: nobody replaces a token
81
+ behind its owner's back.
82
+
6
83
  ## 0.4.1 — the board comes alive: the forge enrols the team, hours stop lying, comments become visible
7
84
 
8
85
  - **A comment now SAYS so, on every open dashboard.** The UI's one write was
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: taskops-cli
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: A shared work board for teams of coding agents, with a human who decides.
5
5
  Project-URL: Repository, https://github.com/bernatch22/taskops
6
6
  License-Expression: MIT
@@ -70,7 +70,7 @@ importable leaves commits un-stamped, silently.
70
70
  taskops init a local board in this repo
71
71
  taskops join [<name>] [--invite <id>] join a board, install the hooks
72
72
  taskops remote add <url> the host this checkout operates, like git's origin
73
- taskops serve host boards — an events API, no dashboard
73
+ taskops serve host boards — /ui and /git open per declared forge
74
74
  taskops server init bootstrap THIS host: its owner and their ssh key
75
75
  taskops board create|ls|push|pull|rm the boards on a host
76
76
  taskops board visibility|forge who may read one · which repo's team is enrolled
@@ -383,9 +383,11 @@ to bytes that are not in history yet is a pointer to nothing.
383
383
  ```
384
384
 
385
385
  Then `taskops ui` lists it under the chapter's **Reports** tab and renders it
386
- full width, read out of **your own clone** at that sha — the dashboard never
387
- asks the server for the bytes, and there is nothing to serve: a host running
388
- `taskops serve` answers `/git` with a 404, whole.
386
+ full width, read out of **your own clone** at that sha — the local dashboard
387
+ never asks the server for the bytes. A host running `taskops serve` answers
388
+ `/git` too, from a bare read-only mirror of the board's DECLARED forge — the
389
+ hosted window, for the reader with no clone. A board that never declared one
390
+ refuses with a sentence naming `taskops board forge <owner>/<repo>`.
389
391
 
390
392
  **Reading one, from the other side.** Everything above is the author's half; a
391
393
  reader needs no ceremony at all. Three doors onto the same bytes:
@@ -58,7 +58,7 @@ importable leaves commits un-stamped, silently.
58
58
  taskops init a local board in this repo
59
59
  taskops join [<name>] [--invite <id>] join a board, install the hooks
60
60
  taskops remote add <url> the host this checkout operates, like git's origin
61
- taskops serve host boards — an events API, no dashboard
61
+ taskops serve host boards — /ui and /git open per declared forge
62
62
  taskops server init bootstrap THIS host: its owner and their ssh key
63
63
  taskops board create|ls|push|pull|rm the boards on a host
64
64
  taskops board visibility|forge who may read one · which repo's team is enrolled
@@ -371,9 +371,11 @@ to bytes that are not in history yet is a pointer to nothing.
371
371
  ```
372
372
 
373
373
  Then `taskops ui` lists it under the chapter's **Reports** tab and renders it
374
- full width, read out of **your own clone** at that sha — the dashboard never
375
- asks the server for the bytes, and there is nothing to serve: a host running
376
- `taskops serve` answers `/git` with a 404, whole.
374
+ full width, read out of **your own clone** at that sha — the local dashboard
375
+ never asks the server for the bytes. A host running `taskops serve` answers
376
+ `/git` too, from a bare read-only mirror of the board's DECLARED forge — the
377
+ hosted window, for the reader with no clone. A board that never declared one
378
+ refuses with a sentence naming `taskops board forge <owner>/<repo>`.
377
379
 
378
380
  **Reading one, from the other side.** Everything above is the author's half; a
379
381
  reader needs no ceremony at all. Three doors onto the same bytes:
@@ -1,4 +1,4 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  __title__ = "taskops"
4
- __version__ = "0.4.1" # single source of truth; pyproject reads it dynamically
4
+ __version__ = "0.5.0" # single source of truth; pyproject reads it dynamically
@@ -25,7 +25,7 @@ if TYPE_CHECKING: # a NAME for the annotation, never an import at start-up
25
25
  from ..http.upstream import Upstream
26
26
 
27
27
  from . import window
28
- from .. import _clock
28
+ from .. import _clock, session
29
29
  from .._json import as_object
30
30
  from ..board import DIR, find_root, is_project, read_config
31
31
  from .._errors import TaskopsError
@@ -133,7 +133,13 @@ def _upstream(root: Path) -> Upstream | None:
133
133
  url = str(config.get("url", ""))
134
134
  if not url:
135
135
  return None
136
- token = str(config.get("token", ""))
136
+ # The SESSION, refreshed here if it has run out and refreshable again from
137
+ # inside the window if it runs out while somebody is reading (2026-08-18,
138
+ # `http/upstream.py`): a host session is twelve hours and this window is
139
+ # meant to be left open. `fresh` falls back to the cached token whenever it
140
+ # cannot sign in, and `refresher` is None for a standing bearer — neither
141
+ # replaces a token nobody may replace, which is `session.py`'s own rule.
142
+ token = str(session.fresh(root, config, _clock.now()))
137
143
  if not token and config.get("readonly"):
138
144
  # A viewer's window (`commands.py::_watch`): an Upstream with no bearer.
139
145
  # `Mounts.public` reads exactly this — no token means this window is
@@ -145,7 +151,7 @@ def _upstream(root: Path) -> Upstream | None:
145
151
  f"{root / DIR}/board.json points at {url} but there is no credential in "
146
152
  "remote.json — run: taskops join <url with ?token= or ?invite=>"
147
153
  )
148
- return Remote(url, token)
154
+ return Remote(url, token, refresh=session.refresher(root, config))
149
155
 
150
156
 
151
157
  def _open(url: str) -> int:
@@ -161,6 +161,18 @@ MILESTONE_STATUSES = ("open", "done", "dropped")
161
161
  CLOSED = ("done", "dropped")
162
162
  LEASE_TTL = 900.0 # 15 min; every MCP call renews it
163
163
  PROJECT = "project" # the `task` of board-level events
164
+ EVERYTHING = "*"
165
+ """`milestone=*`: the WHOLE board, said out loud — no chapter in scope.
166
+
167
+ An ABSENT milestone means "resolve it yourself" (`verbs/_facts.py::in_scope`),
168
+ which with exactly one open chapter IS that chapter. Right for an agent, and it
169
+ left the dashboard's "all chapters" unreachable: it sent no `milestone=`, the
170
+ board came back narrowed anyway, and the picker drew a ✓ beside a scope that had
171
+ never been in effect and that clicking could not change (2026-08-18). Absent and
172
+ "all" are two questions; they now have two arguments, and nothing that omitted
173
+ it sees any change. `verbs/activity.py` is where the two genuinely part — it
174
+ REQUIRES a chapter, and answered `milestone * does not exist`, a lookup's answer
175
+ to a question that was never a lookup."""
164
176
 
165
177
 
166
178
  # Fields of a Card that `edited` may target. Anything else is a BadRequest, so
@@ -0,0 +1,132 @@
1
+ """The board's bare mirror — the forge's history, held so a host can answer.
2
+
3
+ ARCHITECTURE §16 ("The hosted window") decided this on 2026-08-30: a board
4
+ host MAY hold `<root>/<board>/mirror.git`, a bare `git clone --mirror` of the
5
+ forge the owner declared with `board forge` — and only then does it answer
6
+ `/git` and `/ui/`. The mirror is DERIVED and disposable in exactly the sense
7
+ `cache.sqlite` is: delete it and `ensure` re-clones; nothing in it is truth
8
+ the forge does not hold. Its ONLY source is that forge — it takes no client
9
+ pushes, it is not a second trunk, and nothing here ever writes to it beyond
10
+ what `clone` and `fetch` are.
11
+
12
+ **The address is derived, never a credential.** The default remote is
13
+ `https://<host>/<repo>.git`, spelled straight out of the declared fact
14
+ (`core/forge.py` owns that shape) — an anonymous clone, which is all a public
15
+ repo needs. A private repo is the owner's business: they point the mirror at
16
+ an ssh remote backed by a deploy key on the host's filesystem (`url=` at
17
+ clone time, or `git remote set-url` in a mirror that already exists), and
18
+ this module neither knows nor stores anything about it. What §11 bans stays
19
+ banned: no stored token, no write credential, no dev credential travelling.
20
+
21
+ **Failures return None/False, never raise into a request.** These functions
22
+ sit behind HTTP doors serving readers who cannot fix the host's network; a
23
+ mirror that cannot answer is a stale page, not a 500. The same reasoning
24
+ gave `remote.py::push` its 10-second budget, and `fetch` inherits the number:
25
+ a refresh is never a gate, so it may never cost more than a moment. The one
26
+ bounded on-demand fetch lives in `refresh_if_missing` — a requested ref that
27
+ is absent buys exactly one fetch, then the answer is whatever is true, stale
28
+ included. Answering stale beats blocking a page on a forge that is down.
29
+
30
+ All subprocess goes through `run.py`, the one module allowed to import it —
31
+ and `run.git` RAISES on a timeout (right for workers, wrong for a request),
32
+ so the catch here is part of the contract, not belt-and-braces.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from pathlib import Path
38
+
39
+ from . import run
40
+ from ..core import forge as forge_facts
41
+ from .._errors import TaskopsError
42
+
43
+ __all__ = ["ensure", "fetch", "refresh_if_missing"]
44
+
45
+ MIRROR = "mirror.git"
46
+ """§16's wording, verbatim: `<root>/<board>/mirror.git`, beside events.jsonl."""
47
+
48
+ FETCH_TIMEOUT = 10.0
49
+ """`remote.py::PUSH_TIMEOUT`'s argument, pointing the other way: a refresh is
50
+ never a gate. Worst case a reader waits ten seconds ONCE and gets the truth
51
+ about this disk — which is what `gitdoor` answers anyway."""
52
+
53
+ CLONE_TIMEOUT = 120.0
54
+ """A first clone moves the whole history, once, at mount time — not per
55
+ request — so it gets `run.TIMEOUT`'s patience rather than `fetch`'s."""
56
+
57
+
58
+ def _url(forge: object) -> str:
59
+ """The declared fact → the anonymous https remote, or "".
60
+
61
+ `forge_facts.understood` already collapses absent, cleared and
62
+ unintelligible to None, so an https URL is only ever built from a fact a
63
+ door would also act on. No token, no user@ — the address IS the whole
64
+ credential story for a public repo.
65
+ """
66
+ fact = forge_facts.understood(forge)
67
+ if fact is None:
68
+ return ""
69
+ return f"https://{fact['host']}/{fact['repo']}.git"
70
+
71
+
72
+ def ensure(board_dir: Path, forge: object, *, url: str = "") -> Path | None:
73
+ """The mirror's path — cloning it into existence if this is the first ask.
74
+
75
+ An EXISTING `mirror.git` is returned untouched, whatever remote it holds:
76
+ that is how an owner's ssh remote (a deploy key's address) survives every
77
+ later call. `url=` overrides the derived address at clone time — the same
78
+ owner's move, and how tests clone from a local fixture instead of the
79
+ network. No forge and no url means no mirror, which is the state every
80
+ board is born in; a clone that fails leaves nothing behind, so the next
81
+ ask is a clean retry rather than a corpse mistaken for a mirror.
82
+ """
83
+ dest = board_dir / MIRROR
84
+ if dest.is_dir():
85
+ return dest
86
+ remote = url or _url(forge)
87
+ if not remote:
88
+ return None
89
+ try:
90
+ result = run.git("clone", "--mirror", remote, str(dest), timeout=CLONE_TIMEOUT)
91
+ except TaskopsError:
92
+ result = None # timeout, or no git at all — a request never hears it
93
+ if result is None or not result.ok:
94
+ return None
95
+ return dest
96
+
97
+
98
+ def fetch(mirror: Path) -> bool:
99
+ """One bounded fetch from the mirror's own remote. True means it ran clean.
100
+
101
+ A `--mirror` clone's fetch refspec already maps every ref one-to-one, so
102
+ plain `git fetch` IS the whole refresh — no refspec spelled here that
103
+ could drift from what clone configured. False covers everything a reader
104
+ cannot fix: a dead remote, a timeout, a directory that stopped being a
105
+ repo. The caller answers with what it has.
106
+ """
107
+ try:
108
+ return run.git("fetch", cwd=mirror, timeout=FETCH_TIMEOUT).ok
109
+ except TaskopsError: # timeout, or no git at all — never the reader's problem
110
+ return False
111
+
112
+
113
+ def _has(mirror: Path, ref: str) -> bool:
114
+ """`^{commit}` is load-bearing: bare `rev-parse --verify <40-hex>` answers
115
+ ok for ANY well-formed sha, present or not — a ref arriving as a full sha
116
+ (which is what a diff door holds) would always read as here."""
117
+ return run.git("rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}", cwd=mirror).ok
118
+
119
+
120
+ def refresh_if_missing(mirror: Path, ref: str) -> bool:
121
+ """Is `ref` here — after at most ONE fetch if it was not?
122
+
123
+ §16's exact promise: a missing ref triggers one bounded on-demand fetch
124
+ before answering stale. A ref already present costs nothing — no network
125
+ touched, so a page over known history never waits on a forge. A ref still
126
+ absent after the fetch is answered False, truthfully: "not here" is the
127
+ truth about this disk, and the door already knows how to say so.
128
+ """
129
+ if _has(mirror, ref):
130
+ return True
131
+ fetch(mirror)
132
+ return _has(mirror, ref)