taskops-cli 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/cache.sqlite-wal +0 -0
  2. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/events.jsonl +8 -0
  3. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/live.sqlite-wal +0 -0
  4. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ARCHITECTURE.md +1 -1
  5. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/CHANGELOG.md +25 -0
  6. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/PKG-INFO +1 -1
  7. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_version.py +1 -1
  8. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/serving.py +1 -1
  9. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/window.py +1 -1
  10. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/bind.py +11 -2
  11. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/remote.py +14 -1
  12. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/gitdoor.py +14 -4
  13. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/handler.py +2 -0
  14. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/static.py +30 -0
  15. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/upstream.py +1 -1
  16. taskops_cli-0.3.2/src/taskops/ui/app.js +60 -0
  17. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_git.py +32 -0
  18. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_topology.py +28 -2
  19. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_ui.py +14 -2
  20. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_window.py +58 -1
  21. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/fixture.json +109 -109
  22. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/forge-links.tsx +18 -5
  23. taskops_cli-0.3.2/ui/smoke/sections/report-markdown.tsx +86 -0
  24. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/report-sandbox.tsx +28 -7
  25. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/App.tsx +8 -0
  26. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/client.ts +5 -1
  27. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Drawer.tsx +9 -2
  28. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Sections.tsx +32 -21
  29. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/tokens.ts +10 -0
  30. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Chapter.tsx +1 -1
  31. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/reports/ReportFrame.tsx +36 -1
  32. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/main.tsx +13 -3
  33. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/types.ts +5 -2
  34. taskops_cli-0.3.1/src/taskops/ui/app.js +0 -60
  35. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.claude/settings.json +0 -0
  36. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.gitattributes +0 -0
  37. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.github/workflows/ci.yml +0 -0
  38. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.github/workflows/workflow.yml +0 -0
  39. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.gitignore +0 -0
  40. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.mcp.json +0 -0
  41. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/cache.sqlite +0 -0
  42. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/cache.sqlite-shm +0 -0
  43. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/live.sqlite +0 -0
  44. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/live.sqlite-shm +0 -0
  45. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board.json +0 -0
  46. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/reports/lifecycle-chapter.html +0 -0
  47. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/reports/reports-chapter.html +0 -0
  48. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/ui.lock +0 -0
  49. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/CLAUDE.md +0 -0
  50. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/README.md +0 -0
  51. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/pyproject.toml +0 -0
  52. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/__init__.py +0 -0
  53. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_clock.py +0 -0
  54. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_errors.py +0 -0
  55. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_ids.py +0 -0
  56. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_json.py +0 -0
  57. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_locate.py +0 -0
  58. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_wire.py +0 -0
  59. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/board.py +0 -0
  60. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/__init__.py +0 -0
  61. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/__main__.py +0 -0
  62. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/admin.py +0 -0
  63. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/claude.py +0 -0
  64. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/commands.py +0 -0
  65. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/enrol.py +0 -0
  66. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/grants.py +0 -0
  67. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/hooks.py +0 -0
  68. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/main.py +0 -0
  69. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/operate.py +0 -0
  70. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/paging.py +0 -0
  71. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/parser.py +0 -0
  72. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/pull.py +0 -0
  73. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/push.py +0 -0
  74. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/remote.py +0 -0
  75. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/rm.py +0 -0
  76. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/watch.py +0 -0
  77. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/wording.py +0 -0
  78. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/__init__.py +0 -0
  79. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/actors.py +0 -0
  80. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/challenge.py +0 -0
  81. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/chapters.py +0 -0
  82. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/event.py +0 -0
  83. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/forge.py +0 -0
  84. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/graph.py +0 -0
  85. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/holding.py +0 -0
  86. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/hours.py +0 -0
  87. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/machine.py +0 -0
  88. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/mentions.py +0 -0
  89. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/replay.py +0 -0
  90. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/reports.py +0 -0
  91. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/review.py +0 -0
  92. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/scope.py +0 -0
  93. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/seams.py +0 -0
  94. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/types.py +0 -0
  95. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/__init__.py +0 -0
  96. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/catchup.py +0 -0
  97. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/claudefiles.py +0 -0
  98. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/diff.py +0 -0
  99. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/install.py +0 -0
  100. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/landing.py +0 -0
  101. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/patch.py +0 -0
  102. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/run.py +0 -0
  103. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/sig.py +0 -0
  104. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/trailer.py +0 -0
  105. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/trees.py +0 -0
  106. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/__init__.py +0 -0
  107. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/admin.py +0 -0
  108. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/auth.py +0 -0
  109. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/feed.py +0 -0
  110. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/github.py +0 -0
  111. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/grants.py +0 -0
  112. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/ingest.py +0 -0
  113. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/login.py +0 -0
  114. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/mounts.py +0 -0
  115. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/removal.py +0 -0
  116. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/rpc.py +0 -0
  117. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/scoped.py +0 -0
  118. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/server.py +0 -0
  119. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/watcher.py +0 -0
  120. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/identity.py +0 -0
  121. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/__init__.py +0 -0
  122. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/__main__.py +0 -0
  123. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/activity.py +0 -0
  124. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/before.py +0 -0
  125. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/boards.py +0 -0
  126. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/boardview.py +0 -0
  127. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/brief.py +0 -0
  128. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/dossier.py +0 -0
  129. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/fields.py +0 -0
  130. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/gitmoves.py +0 -0
  131. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/hello.py +0 -0
  132. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/integrate.py +0 -0
  133. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/render.py +0 -0
  134. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/schema.py +0 -0
  135. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/server.py +0 -0
  136. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/thread.py +0 -0
  137. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/tools.py +0 -0
  138. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/py.typed +0 -0
  139. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/session.py +0 -0
  140. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/__init__.py +0 -0
  141. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/cache.py +0 -0
  142. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/creds.py +0 -0
  143. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/live.py +0 -0
  144. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/log.py +0 -0
  145. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/pubkeys.py +0 -0
  146. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/reviews.py +0 -0
  147. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/server.py +0 -0
  148. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/stores.py +0 -0
  149. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/ui/index.html +0 -0
  150. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/ui/style.css +0 -0
  151. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/__init__.py +0 -0
  152. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_args.py +0 -0
  153. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_cards.py +0 -0
  154. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_context.py +0 -0
  155. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_facts.py +0 -0
  156. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_mentions.py +0 -0
  157. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_rows.py +0 -0
  158. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_stories.py +0 -0
  159. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_waiting.py +0 -0
  160. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/activity.py +0 -0
  161. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/assign.py +0 -0
  162. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/card.py +0 -0
  163. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/events.py +0 -0
  164. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/filed.py +0 -0
  165. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/plan.py +0 -0
  166. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/project.py +0 -0
  167. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/pulse.py +0 -0
  168. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/record.py +0 -0
  169. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/report.py +0 -0
  170. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/review.py +0 -0
  171. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/take.py +0 -0
  172. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/update.py +0 -0
  173. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/__init__.py +0 -0
  174. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/conftest.py +0 -0
  175. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_architecture.py +0 -0
  176. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_claude.py +0 -0
  177. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_core.py +0 -0
  178. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_mcp.py +0 -0
  179. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_report.py +0 -0
  180. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_store.py +0 -0
  181. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_verbs.py +0 -0
  182. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/ui_harness.js +0 -0
  183. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/build.mjs +0 -0
  184. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/index.html +0 -0
  185. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/package-lock.json +0 -0
  186. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/package.json +0 -0
  187. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/main.tsx +0 -0
  188. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/run.mjs +0 -0
  189. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/actors.tsx +0 -0
  190. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/board-columns.tsx +0 -0
  191. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/chapter-landed.tsx +0 -0
  192. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/chapters-open.tsx +0 -0
  193. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/comment-box.tsx +0 -0
  194. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/diff-page.tsx +0 -0
  195. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/dossier.tsx +0 -0
  196. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/event-stream.tsx +0 -0
  197. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/git-diff.tsx +0 -0
  198. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/monitor-panes.tsx +0 -0
  199. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/overlay-escape.tsx +0 -0
  200. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/reviewing-skew.tsx +0 -0
  201. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/section.ts +0 -0
  202. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/swarm-topology.tsx +0 -0
  203. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/thread-closing-note.tsx +0 -0
  204. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/worktrees-index.tsx +0 -0
  205. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections.mjs +0 -0
  206. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/actors/Daysheet.tsx +0 -0
  207. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/actors/DevPanel.tsx +0 -0
  208. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/board/CardTile.tsx +0 -0
  209. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/board/Column.tsx +0 -0
  210. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/CommentBox.tsx +0 -0
  211. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Patch.tsx +0 -0
  212. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Thread.tsx +0 -0
  213. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/split.ts +0 -0
  214. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/AvatarStack.tsx +0 -0
  215. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/Header.tsx +0 -0
  216. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/KpiRail.tsx +0 -0
  217. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
  218. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/TabNav.tsx +0 -0
  219. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/DependencyChain.tsx +0 -0
  220. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/EditSurface.tsx +0 -0
  221. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/EventStream.tsx +0 -0
  222. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
  223. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/LiveLeases.tsx +0 -0
  224. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Mentions.tsx +0 -0
  225. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Pane.tsx +0 -0
  226. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Swarm.tsx +0 -0
  227. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Throughput.tsx +0 -0
  228. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/panels.ts +0 -0
  229. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/shared/Markdown.tsx +0 -0
  230. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/shared/Overlay.tsx +0 -0
  231. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/shared/overlayStack.ts +0 -0
  232. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/format.ts +0 -0
  233. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/links.tsx +0 -0
  234. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/markdown.ts +0 -0
  235. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Actors.tsx +0 -0
  236. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Board.tsx +0 -0
  237. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Monitor.tsx +0 -0
  238. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Reports.tsx +0 -0
  239. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/WorktreeDiff.tsx +0 -0
  240. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Worktrees.tsx +0 -0
  241. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/theme/theme.ts +0 -0
  242. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/theme/tokens.css +0 -0
  243. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/useBoard.ts +0 -0
  244. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/useEvents.ts +0 -0
  245. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/styles/index.css +0 -0
  246. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/tsconfig.json +0 -0
  247. {taskops_cli-0.3.1 → taskops_cli-0.3.2}/uv.lock +0 -0
@@ -772,3 +772,11 @@
772
772
  {"actor":"dev:berna","body":{"into":"ms/production-catches-up-the-host-r","sha":"e8e71eb91f89c02286b29b7b6dce424653b9e87b"},"id":"0476b2b081ed474629c63868cc9a274d","kind":"merged","task":"tk-5c64d5","ts":1786392488.108064}
773
773
  {"actor":"dev:berna","body":{"branch":"ms/the-lifecycle-runs-backwards-too","files":["ARCHITECTURE.md","CLAUDE.md","README.md","src/taskops/cli/commands.py","src/taskops/cli/main.py","src/taskops/cli/parser.py","src/taskops/core/scope.py","src/taskops/http/admin.py","tests/test_topology.py"],"numstat":{"ARCHITECTURE.md":[168,9],"CLAUDE.md":[17,3],"README.md":[21,3],"src/taskops/_wire.py":[45,0],"src/taskops/cli/commands.py":[42,33],"src/taskops/cli/enrol.py":[82,0],"src/taskops/cli/grants.py":[59,0],"src/taskops/cli/hooks.py":[50,0],"src/taskops/cli/main.py":[13,95],"src/taskops/cli/operate.py":[55,38],"src/taskops/cli/parser.py":[149,0],"src/taskops/core/forge.py":[85,0],"src/taskops/core/scope.py":[9,0],"src/taskops/http/admin.py":[22,0],"src/taskops/http/github.py":[180,0],"src/taskops/http/handler.py":[16,1],"src/taskops/http/ingest.py":[10,5],"src/taskops/http/login.py":[12,2],"src/taskops/http/server.py":[20,0],"src/taskops/verbs/project.py":[53,7],"tests/test_topology.py":[703,1],"tests/test_verbs.py":[82,1]},"sha":"ee54aa8aa0a22e381bc1fd0a127ba0f308c475ca","subject":"merge master — two chapters, one CLI, and the section number they both claimed"},"id":"e34f0f942fed0ce549fdb76b159cb21c","kind":"commit","task":"project","ts":1786392613.0}
774
774
  {"actor":"dev:berna","body":{"criteria_met":true,"id":"ms-392423","into":"master","op":"landed","sha":"1dee83b688967650945a93c8305e313d8ac3481f"},"id":"521b59ad4d037fa2dcc66f9e4e119f97","kind":"milestone","task":"project","ts":1786392618.650824}
775
+ {"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board.json",".taskops/board/cache.sqlite",".taskops/board/cache.sqlite-shm",".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite",".taskops/board/live.sqlite-shm",".taskops/board/live.sqlite-wal",".taskops/ui.lock","CHANGELOG.md","src/taskops/_version.py"],"numstat":{".taskops/board.json":[1,0],".taskops/board/cache.sqlite":null,".taskops/board/cache.sqlite-shm":null,".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[774,0],".taskops/board/live.sqlite":null,".taskops/board/live.sqlite-shm":null,".taskops/board/live.sqlite-wal":null,".taskops/ui.lock":[0,0],"CHANGELOG.md":[31,0],"src/taskops/_version.py":[1,1]},"sha":"9b22232ff4c6ac5df0561296365e2f2576e94a0c","subject":"0.3.1 — three chapters, and the flood that made a remote board unusable"},"id":"4b8de6c0d7d4082c5b289221500e9bb2","kind":"commit","task":"project","ts":1786392799.0}
776
+ {"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","src/taskops/gitwork/bind.py","src/taskops/gitwork/remote.py","tests/test_git.py"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[1,0],".taskops/board/live.sqlite-wal":null,"src/taskops/gitwork/bind.py":[11,2],"src/taskops/gitwork/remote.py":[14,1],"tests/test_git.py":[32,0]},"sha":"3ae9dbfd62d036a8bee49a5e33ff045e06151e31","subject":"fix: a push lands on the branch it names, and never on its upstream"},"id":"797d85bd5d8d857922f491f7ff50889a","kind":"commit","task":"project","ts":1786393176.0}
777
+ {"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/ship1"},"id":"117335f0afc0c4057bd978286351cce0","kind":"edited","task":"tk-1dfa16","ts":1786393201.284905}
778
+ {"actor":"agent:berna/ship1","body":{"no_code":true,"reason":"Deployed taskops-cli 0.3.1 (the PyPI artifact, downloaded by the host itself) to taskops.bernardocastro.dev. No repo commit: the deploy touched no code here; the only file written was ~/.claude/reference/infra.md (runbook corrections, outside this repo).\n\nWHERE IT ACTUALLY RUNS — the runbook was wrong and cost ~6 calls to rediscover. 34.71.115.185 (gpu-relay) runs only nginx: proxy_pass http://10.8.0.2:8082 -> LXD device proxy -> container `bernardocastro` -> pm2 `taskops-v2` = ~/taskops-v2-app/.venv/bin/taskops serve --root ~/taskops-v2-server --host 127.0.0.1 --port 2181. Deploy host is `ssh bernardocastro-box`. infra.md now says so.\n\nROLLBACK, recorded BEFORE anything was replaced, and none of the stored wheels qualified: md5 of every taskops/*.py inside each of the 4 wheels in rollback/ vs site-packages gave mismatch=27 / 25 / 2 / 24 — even the newest (…-bffa26) differed in taskops/cli/push.py and taskops/http/ingest.py (the _birth -> _configuration change), i.e. previous deploys saved the wheel they REPLACED, never the one they installed. So the rollback was rebuilt from the live install: verified the dist-info RECORD against disk first (109 files, 0 bad -> the install was coherent, not hand-patched), then zipped site-packages/taskops + taskops-2.0.0a0.dist-info to rollback/20260810-1dfa16-preupgrade/taskops-2.0.0a0-py3-none-any.whl. Proof it IS the pre-upgrade code: re-extracted and compared — 98 .py compared, 0 mismatches, 98 .py in the wheel (no extras either way). Valid wheel filename inside a dated dir, so a rollback is one pip install with no renaming.\n\nTHE SWAP. Shipped to /tmp/tk-1dfa16/dl (never bare /tmp). Host did `pip download --no-deps --only-binary=:all: taskops-cli==0.3.1` itself (md5 592b03d485977ab1305e0f11e8b151de, 368428 bytes); simple index confirmed serving 0.3.1 first. The installed dist was named `taskops` 2.0.0a0 while PyPI ships `taskops-cli` — both own the taskops/ package, so installing over it would have left the old dist-info plus 2.0.0a0-only modules shadowing. `pip uninstall -y taskops` first (site-packages then held only pip), then `pip install --force-reinstall --no-deps`. pip list is now exactly pip + taskops-cli 0.3.1; taskops.__version__ == 0.3.1. pm2 restart taskops-v2; error log empty since.\n\nVERIFIED AGAINST THE HOST (all over https, not the deploy's exit code):\n- /healthz -> {\"ok\": true, \"seq\": 0, \"data\": {\"boards\": 6}}.\n- POST /axion/join/github: before \"nothing at /axion/join/github\" (404); after -> 409 refused \"this board is opened by invite, not by GitHub — its owner opts in by naming the repo whose membership counts: taskops board forge <owner>/<name> --need push\". Milestone criterion 1 met.\n- Second new-version route, /taskops-v2/git/README.md -> \"this host serves boards, not a repository — it was started outside a checkout\"; in 2.0.0a0 both fell through to \"nothing at\". Control: /axion/nope still \"nothing at /axion/nope\", so the 404 path is intact.\n- Six boards read identically to baseline: agenda/axion/notas/probe -> 409 \"no credential — run: taskops join …\" (same as before), taskops-v2 + scratch-tk-bffa26 -> 200 SSE `data: {\"type\":\"hello\"}`.\n- events.jsonl byte-identical across the upgrade, md5 before == after: agenda 35 b84c0a21…, axion 961 b1623f0a…, notas 48 c8bd750d…, probe 17 6c764e52…, taskops-v2 664 a6cee77d…, scratch-tk-bffa26 5 9ff99159…. 6 board dirs, unchanged. Criterion 2 met.\n\nONE SCARE, RESOLVED, NOT ASSUMED: the first /healthz after restart said {\"boards\": 0}. That is not board loss — http/mounts.py::count returns len(self._boards), a LAZY cache filled by stores(), so a just-restarted process legitimately reports 0 and the old process only said 6 because it had been up 29h. Read one feed per board and healthz returned to 6. Read the code before believing the number; documented in infra.md so the next deploy does not roll back over it. No rollback was needed.\n\nASSUMED, NOT VERIFIED: --no-deps is safe because the project declares zero runtime dependencies (pip resolved none anyway). The four 409 boards were verified as reachable reads (refusal is their pre-existing anonymous behaviour and all six Stores opened, per healthz==6), not as authenticated reads — I hold no credential for them. /taskops-v2/ui now answers 410 \"this host serves the board, not the dashboard\"; I had no pre-upgrade baseline for that path, so I cannot say whether it changed, only that it is a deliberate message and not an error.","to":"done"},"id":"ee24a141f16c3140bb79f1923d4df783","kind":"status","task":"tk-1dfa16","ts":1786393748.646178}
779
+ {"actor":"dev:berna","body":{"into":"ms/production-catches-up-the-host-r","sha":"e8e71eb91f89c02286b29b7b6dce424653b9e87b"},"id":"5aa9ee2076f4262b8bc0ac88bb8b9c88","kind":"merged","task":"tk-1dfa16","ts":1786393792.392233}
780
+ {"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/jp1"},"id":"d74b22b355d8797f6fc451e81aa105bc","kind":"edited","task":"tk-d1ff9b","ts":1786393796.008371}
781
+ {"actor":"dev:berna","body":{"text":"STOP — the ground moved under this card. I killed jp1 mid-work on purpose, because it was about to write an onboarding README naming a host the axion board no longer lives on.\n\nThe axion board MOVED, tonight, at Berna's request (the home box was too slow):\n\n from https://taskops.bernardocastro.dev/axion (nginx on the GCP relay → proxy_pass 10.8.0.2 → LXD container on the home box)\n to https://boards.t.bernardocastro.dev/axion (taskops 0.3.1 SERVED BY the GCP relay itself, systemd unit `taskops`, 127.0.0.1:8787 behind nginx)\n\nMeasured, which is why it moved: /healthz is 0.36s on the new host against 0.77s on the old one — the relay→box hop was 178ms each way.\n\nHow it moved, and what survives:\n- `board pull axion` brought 962 events down (\"all 962 event(s) are held here\"), then `board create` + `board push axion` on the new host: \"verified: 962 landed, seq 963\".\n- The OLD host still holds its copy — a pull destroys nothing. Nobody has removed it, and that is Berna's call, not this card's.\n- jp1's work was NOT wasted: it declared the forge before it was stopped (event 925540b6…, ts 1786393854, `{op: forge, repo: cloudacio/Axion, need: push}`), and that event travelled with the 962. The GitHub door is live on the new host — `POST /axion/join/github` answers 400 with the body shape it wants, no longer 409 \"opened by invite\".\n\nSo for whoever picks this up: every URL in the README is `boards.t.bernardocastro.dev`, the forge step is already done and must not be re-declared, and the milestone's rule still holds — every command in the README gets run at least once, and you say plainly which ones you could not run because you are the owner and not JP.\n\nOne papercut found while doing this, worth its own card and NOT this one: bare `taskops board pull` in a joined checkout defaults the board name to the DIRECTORY (`axion-v3`) instead of the board `board.json` names (`axion`), so it refused with \"no board named 'axion-v3'\". `board pull axion` works."},"id":"41b9109c6dd2c3feba08a6c0dd04eeeb","kind":"comment","task":"tk-d1ff9b","ts":1786394715.91416}
782
+ {"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","ARCHITECTURE.md","src/taskops/cli/serving.py","src/taskops/cli/window.py","src/taskops/http/handler.py","src/taskops/http/static.py","src/taskops/http/upstream.py","src/taskops/ui/app.js","tests/test_window.py","ui/src/client.ts","ui/src/main.tsx"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[6,0],".taskops/board/live.sqlite-wal":null,"ARCHITECTURE.md":[1,1],"src/taskops/cli/serving.py":[1,1],"src/taskops/cli/window.py":[1,1],"src/taskops/http/handler.py":[2,0],"src/taskops/http/static.py":[30,0],"src/taskops/http/upstream.py":[1,1],"src/taskops/ui/app.js":null,"tests/test_window.py":[58,1],"ui/src/client.ts":[5,1],"ui/src/main.tsx":[13,3]},"sha":"ab2da80683a6fb3f4358f01364f5f9b2c2aa0fe0","subject":"the window's address is its port — `taskops ui` opens `/`, not `/board/ui/`"},"id":"bdd27bc33f6137028dfab5546ebfcdfa","kind":"commit","task":"project","ts":1786395781.0}
@@ -1406,7 +1406,7 @@ The routes do not change, so the committed bundle is untouched by any of this:
1406
1406
  dev A (laptop) dev B (laptop)
1407
1407
  ┌────────────────────────────┐ ┌────────────────────────────┐
1408
1408
  │ $ taskops ui │ │ $ taskops ui │
1409
- │ /board/ui/ ← the bundle │ │ /board/ui/ ← the bundle
1409
+ │ / ← the bundle │ │ / ← the bundle
1410
1410
  │ /board/rpc ─────┐ │ │ /board/rpc ─────┐ │
1411
1411
  │ /board/git ─ A's clone │ │ /board/git ─ B's clone │
1412
1412
  └──────────────────┼─────────┘ └──────────────────┼─────────┘
@@ -3,6 +3,31 @@
3
3
  The source of truth for release notes — GitHub Releases are extracted from
4
4
  here, never written twice.
5
5
 
6
+ ## 0.3.2 — the window's address, prose that reads as prose, and a link that stays inside
7
+
8
+ - **`taskops ui` opens `http://127.0.0.1:<port>/`.** It used to hand out
9
+ `/board/ui/?token=…`, which leaked two implementation details into the one URL
10
+ a human types — `board` is only the name a window mounts its single board
11
+ under, and `/ui/` was a door on a HOST, which has served no page since the
12
+ dashboard moved to the binary. Worse, the page then rewrote the address bar to
13
+ a path recomputed from that mount, so a refresh landed on `/` and got a JSON
14
+ 404. The board's own routes are unchanged and still hang off `/board`.
15
+ - **A markdown report is rendered as markdown.** `.md` reports — the ones written
16
+ to be read — were served as their own source, hashes and pipes and all: the
17
+ door typed them `text/plain` and the reader dropped them in a `<pre>`. The door
18
+ now answers `text/markdown` and the dashboard draws them with the same renderer
19
+ a card spec and a close note use. It needs no sandbox and gets none: that
20
+ renderer builds React elements and emits no HTML, so raw markup inside a report
21
+ is drawn as the characters it is. A self-contained HTML report still goes in
22
+ the `allow-scripts`-only frame, and an unrecognised type still degrades to
23
+ plain text.
24
+ - **The card's Worktree block opens the worktree, in the dashboard.** It offered
25
+ a forge `compare ↗` — two branches on somebody else's website, rendered for a
26
+ clone that may not be yours. It now opens the worktree diff view, reading your
27
+ own clone, with the card's thread beside it. It needs no forge slug, so a board
28
+ with no GitHub at all has it too.
29
+ - The chapter goal pane got a taller clamp — a long goal scrolls less.
30
+
6
31
  ## 0.3.1 — three chapters: reports, GitHub as an introduction, and the whole lifecycle
7
32
 
8
33
  - **Reports.** A milestone's narration becomes part of the board: `taskops_activity`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskops-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
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
@@ -1,4 +1,4 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  __title__ = "taskops"
4
- __version__ = "0.3.1" # single source of truth; pyproject reads it dynamically
4
+ __version__ = "0.3.2" # single source of truth; pyproject reads it dynamically
@@ -110,7 +110,7 @@ def ui(here: Path) -> int:
110
110
  port = httpd.server_address[1]
111
111
  (root / DIR / "ui.json").write_text(json.dumps({"port": port, "token": token}) + "\n")
112
112
  window.retire_when_idle(httpd.mounts, httpd)
113
- _open(f"http://127.0.0.1:{port}/board/ui/?token={token}")
113
+ _open(f"http://127.0.0.1:{port}/?token={token}")
114
114
  try:
115
115
  httpd.serve_forever()
116
116
  except KeyboardInterrupt:
@@ -104,7 +104,7 @@ def holder(folder: Path, checkout: Path) -> str:
104
104
  f"ctrl-c in its terminal, or wait ~{int(IDLE_SECONDS / 60)} minutes: "
105
105
  "a window with no open tab retires itself"
106
106
  )
107
- return f"http://127.0.0.1:{port}/board/ui/?token={token}"
107
+ return f"http://127.0.0.1:{port}/?token={token}"
108
108
  raise TaskopsError(
109
109
  f"something holds {folder / LOCK} but no window answers on port {port or '?'} — "
110
110
  "it may be starting (run this again) or wedged: find it with "
@@ -144,7 +144,16 @@ def drain(board: Caller, repo: Path) -> int:
144
144
 
145
145
 
146
146
  def push_card(repo: Path, branch: str) -> bool:
147
- """Publish the card's branch so a PR (or the orchestrator) can see it."""
147
+ """Publish the card's branch so a PR (or the orchestrator) can see it.
148
+
149
+ BOTH sides of the refspec, for the reason `remote.push` carries in full: a
150
+ one-sided `origin <branch>` is resolved through `push.default`, and under
151
+ `upstream` that is the tracked branch and not this one. Here the tracking is
152
+ also what `--set-upstream` is about to write, so the wrong destination and
153
+ the record of it would agree with each other.
154
+ """
148
155
  if not trailer.card_of(branch):
149
156
  return False
150
- return run.git("push", "--set-upstream", "origin", branch, cwd=repo).ok
157
+ return run.git(
158
+ "push", "--set-upstream", "origin", f"{branch}:refs/heads/{branch}", cwd=repo
159
+ ).ok
@@ -142,6 +142,19 @@ def push(repo: Path, *branches: str, cwd: Path | None = None) -> None:
142
142
  return
143
143
  for name in names:
144
144
  try:
145
- run.git("push", "origin", name, cwd=where, timeout=PUSH_TIMEOUT)
145
+ # BOTH sides of the refspec, always. `git push origin <name>` looks
146
+ # explicit and is not: with no colon, git resolves the destination
147
+ # through `push.default`, and under `upstream` — a setting a human
148
+ # may have in ~/.gitconfig for their own reasons — that destination
149
+ # is the branch's UPSTREAM, not its name. Measured here on
150
+ # 2026-08-10: milestone branches cut in v1 still tracked
151
+ # `origin/main`, so this best-effort visibility push was writing
152
+ # integration branches onto MAIN, in a function that swallows every
153
+ # error, in a repo whose whole point is that only the human lands
154
+ # anything on the trunk. `<name>:refs/heads/<name>` cannot be
155
+ # reinterpreted by any config.
156
+ run.git(
157
+ "push", "origin", f"{name}:refs/heads/{name}", cwd=where, timeout=PUSH_TIMEOUT
158
+ )
146
159
  except TaskopsError: # timeout, or no git at all — never the caller's problem
147
160
  return
@@ -94,6 +94,7 @@ ABSENT = (
94
94
 
95
95
  HTML = "text/html"
96
96
  TEXT = "text/plain"
97
+ MARKDOWN = "text/markdown"
97
98
 
98
99
 
99
100
  def answer(repo: Path | None, tail: str, query: str) -> dict[str, Any]:
@@ -135,9 +136,8 @@ def _file(repo: Path, rev: str, wanted: str) -> dict[str, Any]:
135
136
  reading a report is the UI reading a patch, and a second vocabulary would be
136
137
  a second renderer. `content_type` is a FIELD, never this response's header —
137
138
  every door answers `application/json`, so no path can make THIS origin, the
138
- one the token lives in, serve HTML; the reader sandboxes it. `text/html`
139
- only for a literal `.html`, because a type this door does not know must
140
- degrade to something no renderer will execute."""
139
+ one the token lives in, serve HTML; the reader sandboxes it. The three types
140
+ `_kind` answers are argued there."""
141
141
  path = reports.under(wanted)
142
142
  if not path:
143
143
  raise BadRequest(NOT_A_REPORT.format(path=wanted or "a missing ?path="))
@@ -153,7 +153,7 @@ def _file(repo: Path, rev: str, wanted: str) -> dict[str, Any]:
153
153
  return {
154
154
  "path": path,
155
155
  "rev": sha,
156
- "content_type": HTML if path.endswith(".html") else TEXT,
156
+ "content_type": _kind(path),
157
157
  "text": text,
158
158
  "truncated": cut,
159
159
  "cap": patch.CAP,
@@ -186,3 +186,13 @@ def _param(query: str, key: str) -> str:
186
186
  if name == key:
187
187
  return unquote(value.replace("+", " "))
188
188
  return ""
189
+
190
+
191
+ def _kind(path: str) -> str:
192
+ """The three answers, and the default keeps itself safe: an unrecognised
193
+ type falls to `text/plain`, which every reader draws as characters. Decided
194
+ HERE and never in the browser — `ReportFrame` reads what the door SAYS a
195
+ file is, never the extension it can see, so the two cannot drift apart."""
196
+ if path.endswith(".html"):
197
+ return HTML
198
+ return MARKDOWN if path.endswith(".md") else TEXT
@@ -70,6 +70,8 @@ class Handler(BaseHTTPRequestHandler):
70
70
  self._git(board, tail[4:])
71
71
  elif tail.startswith("ui"):
72
72
  self._static(tail[2:])
73
+ elif (page := static.at_root(self.mounts.ui, self.path)) is not None:
74
+ self._send(200, *page) # a WINDOW serves its page at the ROOT
73
75
  else:
74
76
  self._fail(404, BadRequest(f"nothing at {self.path}"))
75
77
 
@@ -84,6 +84,36 @@ def payload(root: Path, rest: str) -> tuple[bytes, str] | None:
84
84
  return (path.read_bytes(), content_type(path)) if path else None
85
85
 
86
86
 
87
+ def at_root(root: Path | None, path: str) -> tuple[bytes, str] | None:
88
+ """The bundle served at the ROOT of a window — `/`, `/app.js`, `/style.css`.
89
+
90
+ `taskops ui` used to hand out `/board/ui/?token=…`, and that path is an
91
+ implementation detail leaking into the one URL a human types: `board` is
92
+ just the name a window mounts its single board under, and `/ui/` was the
93
+ door on a HOST that no longer serves a page at all. A window is a window;
94
+ its address is its port.
95
+
96
+ Deliberately NOT `resolve`: that one falls back to the index for any
97
+ unknown tail, which is right for a door mounted under `/<board>/ui/` and
98
+ wrong at the root, where it would swallow every mistyped API path and
99
+ answer a mistake with a page. Only real files, plus `/` for the index — so
100
+ `/nope` is still the honest 404 the API gives.
101
+
102
+ `root is None` IS the host switch and the only one there is: `Mounts` sets
103
+ `ui` from the same `repo is not None` that /git and /healthz's identity
104
+ read, so a board host cannot start serving a dashboard by any route,
105
+ including this one — there is no second condition to keep in step.
106
+ """
107
+ if root is None or not root.is_dir():
108
+ return None
109
+ rest = path.partition("?")[0].strip("/")
110
+ base = root.resolve()
111
+ target = (root / INDEX) if not rest else (root / rest).resolve()
112
+ if base != target and base not in target.parents:
113
+ return None # `..%2f..` and symlinks have already collapsed
114
+ return (target.read_bytes(), content_type(target)) if target.is_file() else None
115
+
116
+
87
117
  def answer(root: Path | None, rest: str) -> tuple[int, bytes, str]:
88
118
  """The WHOLE /ui door: (status, body, content-type). `root is None` means
89
119
  this process is a board host — see the module docstring for why that is a
@@ -1,7 +1,7 @@
1
1
  """The remote board a LOCAL window is a window onto — the one thing that differs.
2
2
 
3
3
  `taskops ui` always serves the dashboard from the checkout it stands in. The
4
- routes are the same in both modes (`/board/ui/`, `/board/rpc`, `/board/git/…`),
4
+ routes are the same in both modes (`/` for the page, `/board/rpc`, `/board/git/…`),
5
5
  so the committed bundle never learns which kind of board it is talking to. The
6
6
  whole difference is who answers `/rpc`:
7
7