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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.github/workflows/ci.yml +1 -1
  2. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/cache.sqlite-shm +0 -0
  3. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/cache.sqlite-wal +0 -0
  4. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/events.jsonl +12 -0
  5. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/live.sqlite-shm +0 -0
  6. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/live.sqlite-wal +0 -0
  7. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ARCHITECTURE.md +1 -1
  8. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/CHANGELOG.md +10 -0
  9. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/CLAUDE.md +2 -2
  10. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/PKG-INFO +63 -8
  11. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/README.md +61 -6
  12. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_version.py +1 -1
  13. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/commands.py +21 -10
  14. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_topology.py +28 -0
  15. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.claude/settings.json +0 -0
  16. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.gitattributes +0 -0
  17. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.github/workflows/workflow.yml +0 -0
  18. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.gitignore +0 -0
  19. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.mcp.json +0 -0
  20. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/cache.sqlite +0 -0
  21. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/live.sqlite +0 -0
  22. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board.json +0 -0
  23. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/reports/lifecycle-chapter.html +0 -0
  24. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/reports/reports-chapter.html +0 -0
  25. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/ui.lock +0 -0
  26. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/pyproject.toml +0 -0
  27. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/__init__.py +0 -0
  28. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_clock.py +0 -0
  29. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_errors.py +0 -0
  30. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_ids.py +0 -0
  31. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_json.py +0 -0
  32. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_locate.py +0 -0
  33. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_wire.py +0 -0
  34. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/board.py +0 -0
  35. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/__init__.py +0 -0
  36. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/__main__.py +0 -0
  37. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/admin.py +0 -0
  38. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/claude.py +0 -0
  39. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/enrol.py +0 -0
  40. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/grants.py +0 -0
  41. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/hooks.py +0 -0
  42. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/main.py +0 -0
  43. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/operate.py +0 -0
  44. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/paging.py +0 -0
  45. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/parser.py +0 -0
  46. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/pull.py +0 -0
  47. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/push.py +0 -0
  48. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/remote.py +0 -0
  49. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/rm.py +0 -0
  50. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/serving.py +0 -0
  51. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/watch.py +0 -0
  52. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/window.py +0 -0
  53. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/wording.py +0 -0
  54. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/__init__.py +0 -0
  55. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/actors.py +0 -0
  56. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/challenge.py +0 -0
  57. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/chapters.py +0 -0
  58. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/event.py +0 -0
  59. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/forge.py +0 -0
  60. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/graph.py +0 -0
  61. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/holding.py +0 -0
  62. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/hours.py +0 -0
  63. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/machine.py +0 -0
  64. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/mentions.py +0 -0
  65. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/replay.py +0 -0
  66. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/reports.py +0 -0
  67. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/review.py +0 -0
  68. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/scope.py +0 -0
  69. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/seams.py +0 -0
  70. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/types.py +0 -0
  71. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/__init__.py +0 -0
  72. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/bind.py +0 -0
  73. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/catchup.py +0 -0
  74. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/claudefiles.py +0 -0
  75. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/diff.py +0 -0
  76. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/install.py +0 -0
  77. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/landing.py +0 -0
  78. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/patch.py +0 -0
  79. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/remote.py +0 -0
  80. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/run.py +0 -0
  81. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/sig.py +0 -0
  82. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/trailer.py +0 -0
  83. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/trees.py +0 -0
  84. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/__init__.py +0 -0
  85. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/admin.py +0 -0
  86. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/auth.py +0 -0
  87. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/feed.py +0 -0
  88. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/gitdoor.py +0 -0
  89. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/github.py +0 -0
  90. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/grants.py +0 -0
  91. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/handler.py +0 -0
  92. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/ingest.py +0 -0
  93. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/login.py +0 -0
  94. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/mounts.py +0 -0
  95. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/removal.py +0 -0
  96. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/rpc.py +0 -0
  97. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/scoped.py +0 -0
  98. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/server.py +0 -0
  99. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/static.py +0 -0
  100. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/upstream.py +0 -0
  101. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/watcher.py +0 -0
  102. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/identity.py +0 -0
  103. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/__init__.py +0 -0
  104. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/__main__.py +0 -0
  105. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/activity.py +0 -0
  106. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/before.py +0 -0
  107. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/boards.py +0 -0
  108. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/boardview.py +0 -0
  109. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/brief.py +0 -0
  110. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/dossier.py +0 -0
  111. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/fields.py +0 -0
  112. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/gitmoves.py +0 -0
  113. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/hello.py +0 -0
  114. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/integrate.py +0 -0
  115. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/render.py +0 -0
  116. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/schema.py +0 -0
  117. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/server.py +0 -0
  118. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/thread.py +0 -0
  119. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/tools.py +0 -0
  120. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/py.typed +0 -0
  121. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/session.py +0 -0
  122. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/__init__.py +0 -0
  123. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/cache.py +0 -0
  124. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/creds.py +0 -0
  125. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/live.py +0 -0
  126. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/log.py +0 -0
  127. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/pubkeys.py +0 -0
  128. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/reviews.py +0 -0
  129. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/server.py +0 -0
  130. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/stores.py +0 -0
  131. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/ui/app.js +0 -0
  132. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/ui/index.html +0 -0
  133. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/ui/style.css +0 -0
  134. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/__init__.py +0 -0
  135. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_args.py +0 -0
  136. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_cards.py +0 -0
  137. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_context.py +0 -0
  138. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_facts.py +0 -0
  139. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_mentions.py +0 -0
  140. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_rows.py +0 -0
  141. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_stories.py +0 -0
  142. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_waiting.py +0 -0
  143. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/activity.py +0 -0
  144. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/assign.py +0 -0
  145. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/card.py +0 -0
  146. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/events.py +0 -0
  147. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/filed.py +0 -0
  148. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/plan.py +0 -0
  149. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/project.py +0 -0
  150. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/pulse.py +0 -0
  151. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/record.py +0 -0
  152. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/report.py +0 -0
  153. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/review.py +0 -0
  154. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/take.py +0 -0
  155. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/update.py +0 -0
  156. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/__init__.py +0 -0
  157. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/conftest.py +0 -0
  158. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_architecture.py +0 -0
  159. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_claude.py +0 -0
  160. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_core.py +0 -0
  161. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_git.py +0 -0
  162. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_mcp.py +0 -0
  163. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_report.py +0 -0
  164. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_store.py +0 -0
  165. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_ui.py +0 -0
  166. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_verbs.py +0 -0
  167. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_window.py +0 -0
  168. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/ui_harness.js +0 -0
  169. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/build.mjs +0 -0
  170. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/index.html +0 -0
  171. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/package-lock.json +0 -0
  172. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/package.json +0 -0
  173. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/fixture.json +0 -0
  174. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/main.tsx +0 -0
  175. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/run.mjs +0 -0
  176. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/actors.tsx +0 -0
  177. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/board-columns.tsx +0 -0
  178. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/chapter-landed.tsx +0 -0
  179. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/chapters-open.tsx +0 -0
  180. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/comment-box.tsx +0 -0
  181. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/diff-page.tsx +0 -0
  182. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/dossier.tsx +0 -0
  183. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/event-stream.tsx +0 -0
  184. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/forge-links.tsx +0 -0
  185. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/git-diff.tsx +0 -0
  186. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/monitor-panes.tsx +0 -0
  187. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/overlay-escape.tsx +0 -0
  188. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/report-markdown.tsx +0 -0
  189. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/report-sandbox.tsx +0 -0
  190. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/reviewing-skew.tsx +0 -0
  191. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/section.ts +0 -0
  192. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/swarm-topology.tsx +0 -0
  193. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/thread-closing-note.tsx +0 -0
  194. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/worktrees-index.tsx +0 -0
  195. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections.mjs +0 -0
  196. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/App.tsx +0 -0
  197. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/client.ts +0 -0
  198. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/actors/Daysheet.tsx +0 -0
  199. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/actors/DevPanel.tsx +0 -0
  200. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/board/CardTile.tsx +0 -0
  201. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/board/Column.tsx +0 -0
  202. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/CommentBox.tsx +0 -0
  203. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Drawer.tsx +0 -0
  204. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Patch.tsx +0 -0
  205. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Sections.tsx +0 -0
  206. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Thread.tsx +0 -0
  207. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/split.ts +0 -0
  208. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/tokens.ts +0 -0
  209. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/AvatarStack.tsx +0 -0
  210. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/Header.tsx +0 -0
  211. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/KpiRail.tsx +0 -0
  212. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
  213. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/TabNav.tsx +0 -0
  214. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Chapter.tsx +0 -0
  215. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/DependencyChain.tsx +0 -0
  216. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/EditSurface.tsx +0 -0
  217. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/EventStream.tsx +0 -0
  218. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
  219. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/LiveLeases.tsx +0 -0
  220. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Mentions.tsx +0 -0
  221. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Pane.tsx +0 -0
  222. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Swarm.tsx +0 -0
  223. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Throughput.tsx +0 -0
  224. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/panels.ts +0 -0
  225. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/reports/ReportFrame.tsx +0 -0
  226. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/shared/Markdown.tsx +0 -0
  227. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/shared/Overlay.tsx +0 -0
  228. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/shared/overlayStack.ts +0 -0
  229. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/format.ts +0 -0
  230. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/links.tsx +0 -0
  231. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/main.tsx +0 -0
  232. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/markdown.ts +0 -0
  233. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Actors.tsx +0 -0
  234. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Board.tsx +0 -0
  235. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Monitor.tsx +0 -0
  236. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Reports.tsx +0 -0
  237. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/WorktreeDiff.tsx +0 -0
  238. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Worktrees.tsx +0 -0
  239. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/theme/theme.ts +0 -0
  240. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/theme/tokens.css +0 -0
  241. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/types.ts +0 -0
  242. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/useBoard.ts +0 -0
  243. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/useEvents.ts +0 -0
  244. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/styles/index.css +0 -0
  245. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/tsconfig.json +0 -0
  246. {taskops_cli-0.3.2 → taskops_cli-0.3.3}/uv.lock +0 -0
@@ -2,7 +2,7 @@ name: ci
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main, master]
5
+ branches: [master]
6
6
  pull_request:
7
7
  workflow_call: # release.yml runs these same gates on the tag — no second copy
8
8
 
@@ -780,3 +780,15 @@
780
780
  {"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/jp1"},"id":"d74b22b355d8797f6fc451e81aa105bc","kind":"edited","task":"tk-d1ff9b","ts":1786393796.008371}
781
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
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}
783
+ {"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","CHANGELOG.md","src/taskops/_version.py","src/taskops/http/gitdoor.py","src/taskops/ui/app.js","tests/test_topology.py","tests/test_ui.py","ui/smoke/fixture.json","ui/smoke/sections/forge-links.tsx","ui/smoke/sections/report-markdown.tsx","ui/smoke/sections/report-sandbox.tsx","ui/src/App.tsx","ui/src/components/card/Drawer.tsx","ui/src/components/card/Sections.tsx","ui/src/components/card/tokens.ts","ui/src/components/monitor/Chapter.tsx","ui/src/components/reports/ReportFrame.tsx","ui/src/types.ts"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[1,0],".taskops/board/live.sqlite-wal":null,"CHANGELOG.md":[25,0],"src/taskops/_version.py":[1,1],"src/taskops/http/gitdoor.py":[14,4],"src/taskops/ui/app.js":null,"tests/test_topology.py":[28,2],"tests/test_ui.py":[14,2],"ui/smoke/fixture.json":[109,109],"ui/smoke/sections/forge-links.tsx":[18,5],"ui/smoke/sections/report-markdown.tsx":[86,0],"ui/smoke/sections/report-sandbox.tsx":[28,7],"ui/src/App.tsx":[8,0],"ui/src/components/card/Drawer.tsx":[9,2],"ui/src/components/card/Sections.tsx":[32,21],"ui/src/components/card/tokens.ts":[10,0],"ui/src/components/monitor/Chapter.tsx":[1,1],"ui/src/components/reports/ReportFrame.tsx":[36,1],"ui/src/types.ts":[5,2]},"sha":"e9fff1749b079e11bd8b2840457f0bca54cb3420","subject":"0.3.2 — prose reads as prose, and the card's worktree link stays inside"},"id":"a032a873f75785936dbfce569ab9b07d","kind":"commit","task":"project","ts":1786397681.0}
784
+ {"actor":"dev:berna","body":{"branch":"master","files":[".github/workflows/ci.yml",".taskops/board/cache.sqlite-shm",".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-shm",".taskops/board/live.sqlite-wal","ARCHITECTURE.md","CLAUDE.md"],"numstat":{".github/workflows/ci.yml":[1,1],".taskops/board/cache.sqlite-shm":null,".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[1,0],".taskops/board/live.sqlite-shm":null,".taskops/board/live.sqlite-wal":null,"ARCHITECTURE.md":[1,1],"CLAUDE.md":[2,2]},"sha":"01f165b6a0cf7c578039b8fb05e3542ba4f06a01","subject":"one trunk, named by what it actually is"},"id":"570e34f74dcc2a70f1e7b5fc52bf516f","kind":"commit","task":"project","ts":1786433708.0}
785
+ {"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/jp2"},"id":"3a8a0205d4946d6eee567de2a405cd77","kind":"edited","task":"tk-d1ff9b","ts":1786433782.743027}
786
+ {"actor":"agent:berna/jp2","body":{"no_code":true,"reason":"The work landed in ~/axion-v3 (commit 48f116a, Bernardo Castro <me@bernardocastro.dev>, no trailers), not in this worktree — .taskops/trees/tk-d1ff9b has nothing to commit.\n\nWHAT CHANGED\n- NEW docs/13-taskops.md (Spanish, matches the 00-12 series tone): onboarding for ANY dev joining the repo. Board URL, the move off taskops.bernardocastro.dev with the measured reason, the two-command GitHub join, the invite as the alternative, what `join` installs, the ELEVEN MCP tools, the local dashboard, the four refusals with their verbatim text, and a §8 \"Verificado\" table.\n- README.md: the personal onboarding section is GONE. 16 lines now: what taskops is here, the board URL, the two commands, and a link to docs/13. Added the doc to the Documentación table.\n- CLAUDE.md §5 was also lying (old host, \"GitHub no longer grants board access\", \"the nine MCP tools\", a CLI list missing board pull/rm/forge). Fixed all four and pointed at docs/13.\n\nVERIFIED BY RUNNING (2026-08-11)\n- /healthz: new host 0.40s, old host 0.75s. Both answer 200.\n- POST /axion/join/github with a well-formed body -> 409 \"GitHub rejected that token (401) ... this board opens to whoever has push on cloudacio/Axion\". That IS the forge read back; I did not re-declare it.\n- /axion/ui/ -> 410.\n- uv tool install taskops-cli -> +taskops-cli==0.3.2 (in an isolated UV_TOOL_DIR; berna's own install untouched).\n- taskops remote add https://boards.t.bernardocastro.dev -> ok. taskops join axion (bare) -> \"joined ... as dev:berna. Hooks installed; the board is in MCP.\"\n- taskops invite probe-tk-d1ff9b --board axion -> minted (id 11568a62b8dbd711); taskops revoke --invite -> \"revoked\". Host is back to boards:1, no leftovers.\n- taskops board ls, taskops --help, taskops board --help -> the eleven commands and board's seven actions, both quoted in the doc from the real output.\n- taskops ui -> served 127.0.0.1:8791, GET /?token= -> 200 text/html, /healthz -> version 0.3.2. Process killed, ui.json removed.\n- ssh-keygen -t ed25519, gh auth token -> ok.\n- Tool count derived from mcp/tools.py TOOLS in BOTH 0.3.1 and 0.3.2: ELEVEN (board, card, activity, filed, plan, assign, merge, take, review, update, comment). The README's \"nueve\" was stale.\n\nCOULD NOT RUN, and why (both stated plainly in docs/13 §8)\n1. A real first `taskops join axion --github` by a non-owner. I ran it and the host refused: \"this host already has a principal named 'berna', and GitHub membership does not extend it\". That proves the door exists, not that it enrols. Only JP (push on cloudacio/Axion, no board credential) can close milestone criterion 3.\n2. `taskops board forge cloudacio/Axion --need push` — already declared by jp1; re-running is re-declaring, not verifying. Read back through the 409 above instead.\n\nCORRECTION TO THE BRIEF: the host does NOT run 0.3.2. `systemctl show taskops` on gpu-relay -> /home/berna/taskops/.venv/bin/taskops serve --root /home/berna/taskops-boards --host 127.0.0.1 --port 8787, and importlib.metadata reports 0.3.1. It does not matter for this card (0.3.1 already carries the activity/filed verbs and the forge door), but \"the relay serves 0.3.2\" is wrong. Berna's laptop is also on 0.3.1 — that is why `taskops ui` there still opens /board/ui/ while a fresh install opens /. The doc tells the reader to use the URL the command prints, so it is right on both.","to":"done"},"id":"6a1c0ebf50326cfb2fa1afc7881dc7b3","kind":"status","task":"tk-d1ff9b","ts":1786434450.829515}
787
+ {"actor":"dev:berna","body":{"into":"ms/production-catches-up-the-host-r","sha":"e8e71eb91f89c02286b29b7b6dce424653b9e87b"},"id":"447096a1f3da6140780b0dfd10f2fc6d","kind":"merged","task":"tk-d1ff9b","ts":1786434517.435541}
788
+ {"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","README.md","src/taskops/cli/commands.py","tests/test_topology.py"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[4,0],".taskops/board/live.sqlite-wal":null,"README.md":[61,6],"src/taskops/cli/commands.py":[20,9],"tests/test_topology.py":[28,0]},"sha":"33ad8b0433586e6abf8a8f50c17ff119f82b1c10","subject":"join reads the address the clone already carries — v1's two words, restored"},"id":"7db41a1223bd083e3d4f3dd949d9fc68","kind":"commit","task":"project","ts":1786439728.0}
789
+ {"actor":"dev:berna","body":{"branch":"ms/el-forge-enrola-al-equipo-github","created":1786441801.790267,"criteria":["El dueño corre `taskops board forge <owner>/<repo>` y cada colaborador con el acceso declarado queda enrolado: principal=login, llaves de github.com/<login>.keys — probado end-to-end contra un stub del API y en vivo contra un repo real.","Un dev en un clon entra con `taskops join` a secas: sin token, sin invite, sin flag — su llave ya estaba en allowed_signers.","Re-correr el forge sincroniza: enrola a los nuevos, reporta el drift de los que perdieron acceso (sin revocar solo), y jamás toca al owner.","POST /<board>/join/github, --github y el descubrimiento de token en el join NO EXISTEN más — ni código, ni tests que los nombren, ni docs que los enseñen.","grep por token persistido: cero, en el host y en los checkouts. Suite completa verde, docs de los dos repos diciendo la verdad."],"goal":"Hoy el forge abre una puerta que cada dev tiene que cruzar cargando cosas: un token de GitHub (descubierto, pero suyo), una ssh key que quizás fabricó para esto, y un POST al host en el momento del join. Berna lo dijo exacto: pedir push en el repo Y una llave aparte es redundante — quien pushea por ssh YA tiene una llave, publicada por GitHub en https://github.com/&lt;login&gt;.keys, sin token.\n\nEste capítulo mueve TODA la lógica de GitHub al lado del dueño, donde es trivial: `taskops board forge &lt;owner&gt;/&lt;repo&gt;` pasa de declarar un hecho a SINCRONIZAR el equipo — lista los colaboradores con el acceso declarado (una llamada con el token del dueño, que no se guarda), trae las pubkeys de cada uno del endpoint público, y las enrola en el host. Re-correrlo re-sincroniza. El dev, en su clon, escribe `taskops join` y nada más: su llave ya estaba enrolada, firma y entra. Ni token, ni invite, ni --github, ni ssh-keygen.\n\nCon eso la puerta del dev — POST /&lt;board&gt;/join/github, el flag --github, el descubrimiento de token en join — se BORRA, no se deja comentada. Lo que queda de GitHub: el vocabulario del forge (core/forge.py, qué repo y qué acceso) y el sync del dueño. La regla de la casa no se toca: ningún token de GitHub se guarda nunca, en ningún lado — ahora ni siquiera viaja al host.\n\nLo que este capítulo NO hace: pruning automático. Un principal enrolado por invite (ana, que no es login de GitHub) sería revocado por un sync que nunca la conoció — así que el sync AGREGA y REPORTA el drift (quiénes ya no tienen push, con el comando revoke listo para pegar), y revocar sigue siendo un acto del dueño. Si duele, es el capítulo siguiente.","id":"ms-05f325","op":"create","reviews":false,"rules":["Mutation-check cada fix: romperlo a propósito, UN sitio a la vez, ver caer el test correcto, restaurar — con PYTHONDONTWRITEBYTECODE=1.","Ningún token de GitHub se guarda ni se loguea, nunca: viaja en una llamada del CLI del dueño a api.github.com y muere con ella. Un grep por ghp_/github_token sobre lo persistido es parte de cerrar cada card.","Presupuesto de 200 líneas por módulo — si hay que partir, partir por cohesión, jamás relajar la regla. Los tests de test_architecture.py son la pared.","Borrar es borrar: nada de código comentado, flags muertos ni doors que responden 410. Los tests del código borrado se reemplazan por los del nuevo, y se justifica en el commit qué pinneaban.","Los boards legacy con bearer token siguen siendo una flota: nada de esto puede romper el join por ?token= ni por invite — los test_a_legacy_* son la prueba.","Cada refusal nombra la salida. El sync que encuentra un colaborador sin llaves en GitHub lo REPORTA con el camino (invite), no lo omite en silencio."],"status":"open","title":"El forge enrola al equipo — GitHub es del dueño, y de nadie más"},"id":"20383745733325afad71ca037c1a24da","kind":"milestone","task":"project","ts":1786441801.790267}
790
+ {"actor":"dev:berna","body":{"card":{"after":[],"assignee":"","created":1786441801.787267,"created_by":"dev:berna","criteria":["Mismo lote dos veces deja el mismo estado, probado.","El owner re-enrolado sigue owner; sus llaves se acumulan.","Un no-owner es rechazado con la salida en el mensaje.","allowed_signers rederivado entero tras el lote."],"files":["src/taskops/core/scope.py","src/taskops/http/login.py","src/taskops/store/pubkeys.py","src/taskops/store/server.py","tests/test_topology.py"],"id":"tk-bcf293","labels":[],"milestone":"ms-05f325","parent":null,"priority":0,"review":false,"spec":"La costura de todo el capítulo: una operación de host que recibe [{principal, keys: [líneas ssh-...]}, …] y enrola cada par, para que el CLI del dueño tenga a quién hablarle.\n\nDÓNDE MIRAR primero: `core/scope.py` (board.forge es la pared OWNER exacta a imitar), `http/login.py`::register y `store/pubkeys.py` (el enroll que ya existe — extender, no duplicar: el invite y esto tienen que terminar en el MISMO sitio), `store/server.py` (principals/roles), y el test `test_a_re_join_never_demotes_the_owner_it_only_adds_a_key` — esa propiedad tiene que sobrevivir al lote: enrolar un principal que ya existe AGREGA llaves, jamás reescribe el rol.\n\nSemántica: idempotente (mismo lote dos veces = mismo estado); un principal nuevo entra como member; el owner jamás cambia; allowed_signers se rederiva ENTERO al final, como ya hace todo enroll. La respuesta dice qué hizo: enrolados nuevos, llaves agregadas, sin cambios — números y nombres, porque el CLI los va a imprimir tal cual.\n\nQué NO hace: no revoca a nadie (el porqué está en el goal del milestone — un principal de invite sería víctima de un sync que no lo conoció), no habla con GitHub (no sabe qué es GitHub: recibe principals y llaves), no toca boards (es del HOST, como server init).\n\nTests: la pared de rol (un member que lo intenta es rechazado nombrando la salida), idempotencia, owner intocable, llaves acumulándose, y los legacy intactos.","status":"open","title":"El host enrola en lote — la operación members, del OWNER y de nadie más","updated":1786441801.787267}},"id":"759363f6cae91d1121454bb396118bc9","kind":"created","task":"tk-bcf293","ts":1786441801.787267}
791
+ {"actor":"dev:berna","body":{"card":{"after":["tk-bcf293"],"assignee":"","created":1786441801.788267,"created_by":"dev:berna","criteria":["forge contra el stub enrola a todos los colaboradores con push, paginación incluida.","Un colaborador sin llaves en GitHub queda nombrado con su salida, no omitido.","El drift se reporta con el comando revoke exacto; nada se revoca solo.","El token no aparece en nada persistido ni impreso — grep como parte del cierre."],"files":["src/taskops/cli/operate.py","src/taskops/cli/enrol.py","src/taskops/_wire.py","tests/test_topology.py"],"id":"tk-194ff2","labels":[],"milestone":"ms-05f325","parent":null,"priority":0,"review":false,"spec":"`taskops board forge <owner>/<repo> [--need push|admin]` pasa de declarar a DECLARAR+SINCRONIZAR, y re-correrlo re-sincroniza.\n\nEl flujo del lado del dueño, todo en el CLI: (1) el token del dueño — MOVER `enrol.github_token()` a donde corresponda ahora, no reescribirlo: gh auth token → $GITHUB_TOKEN → prompt oculto, jamás un valor de flag; (2) GET /repos/<o>/<r>/collaborators con el permiso declarado (paginado — un equipo no entra en una página; api.github.com via _wire); (3) por cada login, GET https://github.com/<login>.keys — público, sin token; (4) la operación members de la card anterior, un solo lote; (5) imprimir el reporte: enrolados, llaves agregadas, SIN LLAVES EN GITHUB (nombrados, con `taskops invite <login>` como salida), y el DRIFT — principals del host que ya no están en la lista de colaboradores, con el `taskops revoke --key SHA256:…` listo para pegar y la aclaración de que un principal de invite puede aparecer ahí legítimamente.\n\nEl token muere con la llamada (2). No se guarda, no se loguea, no viaja al host — el host recibe principals y llaves, nunca sabe de GitHub.\n\nDÓNDE MIRAR: `cli/operate.py` (forge hoy), `cli/enrol.py` (github_token — se muda con su docstring, que es el post-mortem del flag), `core/forge.py` (el vocabulario, no debería cambiar), `_wire.py`. Tests con un stub del API de GitHub sobre socket real — el patrón ya existe en test_topology para el stub forge; los tests del stub viejo que prueban MEMBRESÍA del lado del host van a morir en la card siguiente, los tuyos prueban el SYNC del lado del CLI.\n\nOjo con el presupuesto de líneas en operate.py — si el sync no entra, módulo propio (cli/sync.py o similar), partido por cohesión.","status":"open","title":"El forge sincroniza — collaborators con push → .keys → enroll, y el drift reportado","updated":1786441801.788267}},"id":"1ce93c8f500604691a443f56a3dc9f54","kind":"created","task":"tk-194ff2","ts":1786441801.788267}
792
+ {"actor":"dev:berna","body":{"card":{"after":["tk-194ff2"],"assignee":"","created":1786441801.789267,"created_by":"dev:berna","criteria":["grep -r 'join/github\\|--github\\|by_github' sobre src/ y tests/: cero.","El e2e nuevo: sync enrola → clon fresco → `taskops join` a secas → adentro.","Los legacy (?token=, invite) siguen verdes sin tocar sus tests.","handler.py y parser.py más chicos que antes del capítulo."],"files":["src/taskops/http/github.py","src/taskops/http/handler.py","src/taskops/cli/parser.py","src/taskops/cli/commands.py","src/taskops/cli/enrol.py","tests/test_topology.py"],"id":"tk-4c9761","labels":[],"milestone":"ms-05f325","parent":null,"priority":0,"review":false,"spec":"Con el sync vivo, la puerta del dev es peso muerto y se BORRA entera: `http/github.py` (el módulo), su ruta en `http/handler.py` (POST /<board>/join/github), el flag `--github` de `cli/parser.py`, la rama github de `commands.join`, `enrol.by_github` + NO_KEY + NO_TOKEN (github_token ya se mudó en la card anterior — verificar que nada del lado dev lo importe).\n\nEl join queda: bare (dirección del clon o remote grabado) con llave · --invite · ?token= legacy · público read-only. Un dev cuya llave enroló el sync entra con `taskops join` a secas — probarlo end-to-end: stub-sync enrola, clon fresco con board.json, join bare, firma, entra. `--as <login>` para el unix user que no coincide: ya existe, solo verificar que el camino funciona.\n\nLOS TESTS DEL CÓDIGO BORRADO: los que pineaban la puerta (el stub forge de test_topology que responde membresía, los test del door 400/409, BOTH invite+github) se borran CON justificación en el commit — pineaban un contrato que este capítulo retira a propósito. Los que pinean cosas que sobreviven (github_token nunca en un flag, el token muere con la llamada) se MUDAN al lado del sync si la card anterior no lo hizo ya. El refusal de un board sin forge (409 invite-only) también muere con la ruta.\n\nCuidado quirúrgico con handler.py y parser.py: presupuestos 191/149, borrar debería BAJARLOS. mcp/schema o tools no nombran --github (verificar con grep). El hecho op=forge y core/forge.py QUEDAN — son el vocabulario del sync ahora.","status":"open","title":"La puerta del dev se borra — join sin GitHub, de verdad","updated":1786441801.789267}},"id":"5ccccc43f25f0e7c696f926524b00574","kind":"created","task":"tk-4c9761","ts":1786441801.789267}
793
+ {"actor":"dev:berna","body":{"card":{"after":["tk-4c9761"],"assignee":"","created":1786441801.790267,"created_by":"dev:berna","criteria":["grep --github/join/github en docs de ambos repos: cero, salvo historia (CHANGELOG viejo).","ARCHITECTURE §19 cuenta el diseño nuevo con la fecha; §11 endurecido.","El board de axion sincronizado, con el reporte pegado en la card — o el bloqueo dicho con el comando listo.","Cada comando de los docs, corrido; los que no, nombrados."],"files":["README.md","ARCHITECTURE.md","CHANGELOG.md","CLAUDE.md"],"id":"tk-eb8453","labels":[],"milestone":"ms-05f325","parent":null,"priority":1,"review":false,"spec":"Todo lo que enseña el flujo viejo, corregido en el MISMO capítulo que lo cambió — un doc que enseña --github un día más es un onboarding roto para el próximo que clone.\n\n(1) taskops-v2: README (la escalera del join, la sección '### The whole flow' con su diagrama — el POST del dev desaparece, el sync del dueño entra; la lista de board actions; el bloque del forge), ARCHITECTURE.md §19 se reescribe: el título 'GitHub is the INTRODUCTION, never the credential' evoluciona a su forma final — GitHub es el ROSTER, preguntado por el dueño; el dev no carga nada. La entrada de §11 (STORED GitHub token) queda MÁS fuerte: ahora ni viaja. CLAUDE.md del repo si nombra el flujo. CHANGELOG: sección nueva SIN número de versión — el número lo elige Berna.\n\n(2) axion-v3: docs/13-taskops.md (el onboarding entero se simplifica: `taskops join` y reiniciar Claude — la sección --github muere), README (el bloque de dos líneas queda en una), CLAUDE.md §5 si lo nombra. Identidad de commit: Bernardo Castro <me@bernardocastro.dev>, sin trailers.\n\n(3) El board REAL de axion: correr el forge sync contra cloudacio/Axion en boards.t.bernardocastro.dev y pegar el reporte en la card — quiénes quedaron enrolados. Si el token de gh activo no alcanza para listar colaboradores de cloudacio, DECIRLO y dejar el comando exacto para que Berna lo corra — no inventar acceso.\n\n(4) Si algún .tsx cambió en el capítulo (no debería — el header del forge sigue igual), UNA rebuild del bundle acá. Verificar con git diff antes de asumir.\n\nRegla de siempre: cada comando que aparezca en un doc, corrido al menos una vez, y decir cuáles no pudiste.","status":"open","title":"Cierre — los docs dejan de enseñar la puerta que no existe, y axion se re-forja","updated":1786441801.790267}},"id":"d9c06ea6386110cf2efa70c61a9be318","kind":"created","task":"tk-eb8453","ts":1786441801.790267}
794
+ {"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/lote1"},"id":"7d26319e1285adef7fe667287cf0ac7a","kind":"edited","task":"tk-bcf293","ts":1786441826.92571}
@@ -836,7 +836,7 @@ nobody has touched yet.
836
836
  |---|---|---|
837
837
  | a `recover` verb | doing is derived from the live lease; nothing is ever wrong to recover | no entry in `verbs/__init__.py::REGISTRY`; `tests/test_verbs.py::test_a_dead_workers_card_comes_back_by_itself` |
838
838
  | a reviewer ROLE, a stored review STATUS, automatic reviewer assignment | v1's review system: `peer` deadlocks, 14 closing rules over 6 modules, reviewers eating the budget of the work | review EXISTS since 2026-08-07 but narrowed: optional per card, derived from history-only events + a second lease, judged by an ordinary agent that may never judge its own work. `CARD_STATUSES` stays three; there is no reviewer role and nothing auto-assigns |
839
- | AUTOMATIC merges to `main` | v1's `land` merged as a side effect of closing a card and ran checkout under working agents | a CARD cannot be merged to main — `taskops_merge task=` takes no target. A finished MILESTONE lands via `taskops_merge milestone=` (2026-08-07): explicit, refused while any card is open or unintegrated, refused off-trunk, recorded as a `milestone landed` event. What stays impossible is main moving as a side effect of anything |
839
+ | AUTOMATIC merges to the trunk | v1's `land` merged as a side effect of closing a card and ran checkout under working agents | a CARD cannot be merged to the trunk — `taskops_merge task=` takes no target. A finished MILESTONE lands via `taskops_merge milestone=` (2026-08-07): explicit, refused while any card is open or unintegrated, refused off-trunk, recorded as a `milestone landed` event. What stays impossible is the trunk moving as a side effect of anything |
840
840
  | git replication between clones | split-brain, two machines "owning" the same card | `RemoteBoard` never falls back to a local store on write failure (`Unreachable` instead) |
841
841
  | Claude hooks **that decide or store** | latency, another thing to install and drift; v1's held state and gated actions | context travels in `initialize.instructions` + tool responses. The ONE exception, sanctioned 2026-08-06: `taskops hook claude`, delivery-only — it reads, injects a ✉ or ◆ line, and can be deleted with no loss but immediacy. `tests/test_claude.py` pins its safety properties, one test each — the module docstring lists them and `grep -c '^def test_' tests/test_claude.py` counts them |
842
842
  | a mark-as-read / ack verb for mentions | a stored `read` flag is `recover` again: a write whose only job is to contradict an earlier one | `core/mentions.py::pending()` derives it from the thread; `tests/test_verbs.py::test_a_mention_clears_itself_the_moment_the_actor_touches_the_card` |
@@ -3,6 +3,16 @@
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.3 — a clone joins with two words
7
+
8
+ - **`taskops join` reads the address the clone already carries.** v2 committed
9
+ `.taskops/board.json` exactly as v1 did — and never read it back: a bare join
10
+ resolved through the per-machine recorded remote, so a fresh clone died on
11
+ "which host?" while holding the answer in its tree. Restored: inside a clone,
12
+ `taskops join` (or `taskops join --github`, first time) is the whole
13
+ onboarding. `taskops remote add` remains for a checkout that carries no
14
+ address, or a join onto a different board.
15
+
6
16
  ## 0.3.2 — the window's address, prose that reads as prose, and a link that stays inside
7
17
 
8
18
  - **`taskops ui` opens `http://127.0.0.1:<port>/`.** It used to hand out
@@ -37,7 +37,7 @@ releases its card by definition. A stalled card is handed over with
37
37
  **2. Branches are inhabited, not switched.** `git switch` appears nowhere.
38
38
 
39
39
  ```
40
- main ────────────────────────────▶ the HUMAN decides: a PR, or taskops_merge milestone=
40
+ master ──────────────────────────▶ the HUMAN decides: a PR, or taskops_merge milestone=
41
41
  └─ ms/<slug> ──┬──────┬───────▶ the ORCHESTRATOR integrates, card by card
42
42
  tk-a11 tk-b22 ← one WORKER each, one worktree each
43
43
  ```
@@ -202,7 +202,7 @@ what it exists to report, not a fault.
202
202
 
203
203
  Each has its line in ARCHITECTURE.md §11 saying what it cost and where it is
204
204
  enforced: a reviewer ROLE, a stored review STATUS, or automatic reviewer
205
- assignment · `land` or automatic merges to main · git replication between clones
205
+ assignment · `land` or automatic merges to the trunk · a SECOND trunk (2026-08-10: `main` and `master` both existed, `trees.base_ref` cut every chapter from `origin/main`, and a one-sided push refspec was quietly landing card merges there — three facts that only became a bug together) · git replication between clones
206
206
  · Claude hooks **that decide or store** · a stored `doing` · a slug in a branch
207
207
  name · a `recover` · a mark-as-read/ack verb · per-request SIGNING · hand-rolled
208
208
  crypto or a pip crypto dependency · a `--force` on `board push` **or on `board
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: taskops-cli
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -47,8 +47,9 @@ can be misread:
47
47
  into it exactly once, to install it and register your key.
48
48
  * **A board**: one directory on that host. Created from your laptop with
49
49
  `taskops board create`.
50
- * **A repo**: joined to one board. `taskops remote add` once per checkout, then
51
- every command runs bare.
50
+ * **A repo**: joined to one board. A clone carries the board's address
51
+ (`.taskops/board.json`, committed), so `taskops join` is the whole step;
52
+ `taskops remote add` covers a checkout that carries none.
52
53
  * **Your agents**: they never touch any of this — they talk to the board through
53
54
  the eleven MCP tools.
54
55
 
@@ -90,10 +91,11 @@ Joining a hosted one is bare, like every other verb — the host is recorded onc
90
91
  and the key is discovered the way ssh discovers one:
91
92
 
92
93
  ```sh
93
- taskops remote add https://host:8787 # once per checkout
94
- taskops join my-project # your key is registered: that is the credential
95
- taskops join my-project --invite <id> # first time: the invite enrols your key too
96
- taskops join my-project --github # first time, no invite: GitHub vouches for you
94
+ taskops join # a clone: board.json carries the address, done
95
+ taskops join --github # same clone, first time: GitHub vouches for you
96
+ taskops join my-project --invite <id> # first time by invite: it enrols your key too
97
+ taskops remote add https://host:8787 # no carried address? record the host once…
98
+ taskops join my-project # …and name the board
97
99
  taskops join my-project # no key + public board: read-only window
98
100
  ```
99
101
 
@@ -222,6 +224,59 @@ until an owner records it and cleared again with `--clear`. Only a board that
222
224
  has been opted in answers `--github` at all; every other one is invite-only,
223
225
  exactly as before.
224
226
 
227
+ ### The whole flow, and why cloning is not enough
228
+
229
+ Cloning the repo gives you the board's ADDRESS — `.taskops/board.json` is
230
+ committed and travels with the code — but the host has never seen you: it is a
231
+ different server from GitHub, sharing no session and no cookie with it. So one
232
+ command — `taskops join --github`, no URL: the clone already carries it —
233
+ introduces you, once per checkout, and after that GitHub is not in the picture
234
+ at all.
235
+
236
+ ```
237
+ GitHub the board HOST
238
+ <owner>/<repo>, private principals + allowed_signers
239
+ | |
240
+ 1. git clone -> the code, and .taskops/board.json (the address).
241
+ remote.json is 0600 and gitignored: no credential travels.
242
+ | |
243
+ 2. taskops join --github (the carried address) |
244
+ | |
245
+ |-- the CLI finds YOUR token: `gh auth token`, else $GITHUB_TOKEN,
246
+ | else a hidden prompt. Never a flag value — the shell writes those
247
+ | into ~/.zsh_history before the process starts.
248
+ | |
249
+ |-- POST /<board>/join/github ------------------>|
250
+ | { github_token, principal, pubkey } |
251
+ | |
252
+ | the host asks GitHub ONCE, with your token:
253
+ | "does this user have <need> on <owner>/<repo>?"
254
+ | |
255
+ | yes -> it writes exactly two rows: |
256
+ | principals: <you>, member
257
+ | allowed_signers: <you> ssh-ed25519 AAAA...
258
+ | no -> refused, without saying whether the repo exists
259
+ | |
260
+ 3. the GitHub token dies with that request. It is on neither disk.
261
+
262
+ from here on GitHub never participates again:
263
+
264
+ every session: your key signs a challenge -> the host checks it
265
+ (~/.ssh/id_ed25519) against allowed_signers
266
+ -> a 12h session
267
+ ```
268
+
269
+ The alternative — re-checking GitHub on every request — would mean the host
270
+ holding somebody else's token, or being handed one on each call. That is the
271
+ whole category of problem this removes: there is no token to steal because
272
+ there is none stored.
273
+
274
+ **One consequence, stated plainly: access is granted automatically and taken
275
+ back by hand.** Losing push on the repo does not close the board, because the
276
+ credential is no longer GitHub — it is the enrolled key. Remove it with
277
+ `taskops revoke --key SHA256:…`, which is the same verb an invite-enrolled key
278
+ takes.
279
+
225
280
  No URL and no `--key` after `remote add`: the host is recorded in the checkout,
226
281
  `board create` records the name, and the key is **discovered** the way ssh
227
282
  discovers one — `~/.ssh/id_ed25519`, `id_ecdsa`, `id_rsa`, in that order.
@@ -35,8 +35,9 @@ can be misread:
35
35
  into it exactly once, to install it and register your key.
36
36
  * **A board**: one directory on that host. Created from your laptop with
37
37
  `taskops board create`.
38
- * **A repo**: joined to one board. `taskops remote add` once per checkout, then
39
- every command runs bare.
38
+ * **A repo**: joined to one board. A clone carries the board's address
39
+ (`.taskops/board.json`, committed), so `taskops join` is the whole step;
40
+ `taskops remote add` covers a checkout that carries none.
40
41
  * **Your agents**: they never touch any of this — they talk to the board through
41
42
  the eleven MCP tools.
42
43
 
@@ -78,10 +79,11 @@ Joining a hosted one is bare, like every other verb — the host is recorded onc
78
79
  and the key is discovered the way ssh discovers one:
79
80
 
80
81
  ```sh
81
- taskops remote add https://host:8787 # once per checkout
82
- taskops join my-project # your key is registered: that is the credential
83
- taskops join my-project --invite <id> # first time: the invite enrols your key too
84
- taskops join my-project --github # first time, no invite: GitHub vouches for you
82
+ taskops join # a clone: board.json carries the address, done
83
+ taskops join --github # same clone, first time: GitHub vouches for you
84
+ taskops join my-project --invite <id> # first time by invite: it enrols your key too
85
+ taskops remote add https://host:8787 # no carried address? record the host once…
86
+ taskops join my-project # …and name the board
85
87
  taskops join my-project # no key + public board: read-only window
86
88
  ```
87
89
 
@@ -210,6 +212,59 @@ until an owner records it and cleared again with `--clear`. Only a board that
210
212
  has been opted in answers `--github` at all; every other one is invite-only,
211
213
  exactly as before.
212
214
 
215
+ ### The whole flow, and why cloning is not enough
216
+
217
+ Cloning the repo gives you the board's ADDRESS — `.taskops/board.json` is
218
+ committed and travels with the code — but the host has never seen you: it is a
219
+ different server from GitHub, sharing no session and no cookie with it. So one
220
+ command — `taskops join --github`, no URL: the clone already carries it —
221
+ introduces you, once per checkout, and after that GitHub is not in the picture
222
+ at all.
223
+
224
+ ```
225
+ GitHub the board HOST
226
+ <owner>/<repo>, private principals + allowed_signers
227
+ | |
228
+ 1. git clone -> the code, and .taskops/board.json (the address).
229
+ remote.json is 0600 and gitignored: no credential travels.
230
+ | |
231
+ 2. taskops join --github (the carried address) |
232
+ | |
233
+ |-- the CLI finds YOUR token: `gh auth token`, else $GITHUB_TOKEN,
234
+ | else a hidden prompt. Never a flag value — the shell writes those
235
+ | into ~/.zsh_history before the process starts.
236
+ | |
237
+ |-- POST /<board>/join/github ------------------>|
238
+ | { github_token, principal, pubkey } |
239
+ | |
240
+ | the host asks GitHub ONCE, with your token:
241
+ | "does this user have <need> on <owner>/<repo>?"
242
+ | |
243
+ | yes -> it writes exactly two rows: |
244
+ | principals: <you>, member
245
+ | allowed_signers: <you> ssh-ed25519 AAAA...
246
+ | no -> refused, without saying whether the repo exists
247
+ | |
248
+ 3. the GitHub token dies with that request. It is on neither disk.
249
+
250
+ from here on GitHub never participates again:
251
+
252
+ every session: your key signs a challenge -> the host checks it
253
+ (~/.ssh/id_ed25519) against allowed_signers
254
+ -> a 12h session
255
+ ```
256
+
257
+ The alternative — re-checking GitHub on every request — would mean the host
258
+ holding somebody else's token, or being handed one on each call. That is the
259
+ whole category of problem this removes: there is no token to steal because
260
+ there is none stored.
261
+
262
+ **One consequence, stated plainly: access is granted automatically and taken
263
+ back by hand.** Losing push on the repo does not close the board, because the
264
+ credential is no longer GitHub — it is the enrolled key. Remove it with
265
+ `taskops revoke --key SHA256:…`, which is the same verb an invite-enrolled key
266
+ takes.
267
+
213
268
  No URL and no `--key` after `remote add`: the host is recorded in the checkout,
214
269
  `board create` records the name, and the key is **discovered** the way ssh
215
270
  discovers one — `~/.ssh/id_ed25519`, `id_ecdsa`, `id_rsa`, in that order.
@@ -1,4 +1,4 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  __title__ = "taskops"
4
- __version__ = "0.3.2" # single source of truth; pyproject reads it dynamically
4
+ __version__ = "0.3.3" # single source of truth; pyproject reads it dynamically
@@ -15,7 +15,7 @@ from pathlib import Path
15
15
  from . import enrol, watch
16
16
  from .. import session, identity
17
17
  from .._json import query
18
- from ..board import DIR, find_root, open_board
18
+ from ..board import DIR, find_root, open_board, read_config
19
19
  from ..store import log
20
20
  from .._errors import TaskopsError
21
21
  from ..gitwork import remote, install, claudefiles
@@ -58,13 +58,15 @@ def join( # noqa: PLR0913 — one command, one config; each flag is a way IN
58
58
  invite: str = "",
59
59
  github: bool = False,
60
60
  ) -> int:
61
- """Connect this repo to a board. Bare like every other verb since the host is
62
- recorded and the key is discovered:
63
-
64
- taskops remote add https://host:8787 once per checkout
65
- taskops join my-project registered key: signs in, done
66
- taskops join my-project --invite <id> first time: enrols the key too
67
- taskops join my-project --github first time, no invite: GitHub vouches
61
+ """Connect this repo to a board. Bare like every other verb: the address is
62
+ CARRIED by the clone, the key is discovered, and the recorded remote is the
63
+ fallback for a checkout that carries nothing:
64
+
65
+ taskops join a clone: board.json travels, done
66
+ taskops join --github same clone, first time: GitHub vouches
67
+ taskops join my-project --invite <id> first time by invite: enrols the key
68
+ taskops remote add https://host:8787 no carried address? record the host…
69
+ taskops join my-project …and name the board
68
70
  taskops join my-project no key anywhere + public board: read-only
69
71
 
70
72
  The name defaults exactly as `board create`'s does (recorded name, else the
@@ -93,8 +95,17 @@ def join( # noqa: PLR0913 — one command, one config; each flag is a way IN
93
95
  root = find_root(here)
94
96
  bare = "://" not in target
95
97
  if bare:
96
- host, name = remote_cli.named(target)
97
- target = f"{host}/{name}"
98
+ # v1's whole ambition, restored (its join.py said it in one line: "a
99
+ # clone carries `.taskops/board.json`, so the second developer types
100
+ # two words"). The committed address is read FIRST, so a fresh clone
101
+ # joins with no URL and no `remote add` — the recorded remote is for
102
+ # the checkout that has no carried address, or a DIFFERENT board.
103
+ carried = str(read_config(root).get("url", ""))
104
+ if carried and target in ("", carried.rsplit("/", 1)[-1]):
105
+ target = carried
106
+ else:
107
+ host, name = remote_cli.named(target)
108
+ target = f"{host}/{name}"
98
109
  base = target.partition("?")[0]
99
110
  params = query(target)
100
111
  invite = invite or params.get("invite", "")
@@ -665,6 +665,34 @@ def test_an_invite_registers_the_joiners_key_and_still_answers_a_token(
665
665
  assert RemoteBoard(url_of(server), minted["token"], ANA).call("board", {})["seq"] >= 0
666
666
 
667
667
 
668
+ def test_a_clone_that_carries_its_boards_address_joins_bare_no_url_no_remote(
669
+ server: BoardServer, keyed: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
670
+ ) -> None:
671
+ """v1's two-word join, restored (2026-08-11). `.taskops/board.json` is
672
+ COMMITTED and travels with the clone, so the second developer types
673
+ `taskops join` and nothing else — no URL pasted from a chat, no
674
+ `remote add` first. The regression: v2 resolved a bare join through the
675
+ RECORDED remote (`remote.json`, per-machine, never travels) and ignored the
676
+ address the clone already carried. The recorded remote stays the fallback
677
+ for a checkout with no carried address, or a join onto a DIFFERENT board.
678
+
679
+ Both bare spellings, because both read the carried address: no target at
680
+ all, and the board's own name (which must not fall through to `named()`
681
+ and die on the missing remote)."""
682
+ from taskops.cli import commands
683
+
684
+ for target in ("", BOARD):
685
+ project = tmp_path / f"clone-{target or 'bare'}"
686
+ (project / ".git").mkdir(parents=True)
687
+ (project / ".taskops").mkdir()
688
+ (project / ".taskops" / "board.json").write_text(json.dumps({"url": url_of(server)}))
689
+ monkeypatch.setenv("TASKOPS_ACTOR", BERNA)
690
+ commands.join(project, target, BERNA, str(keyed))
691
+ saved = json.loads((project / ".taskops" / "remote.json").read_text())
692
+ assert saved["login"]["host"] == host_of(server)
693
+ assert RemoteBoard(url_of(server), saved["token"], BERNA).call("board", {})["seq"] >= 0
694
+
695
+
668
696
  def test_a_re_join_never_demotes_the_owner_it_only_adds_a_key(
669
697
  server: BoardServer, keyed: Path, tmp_path: Path
670
698
  ) -> None:
File without changes
File without changes
File without changes
File without changes