taskops-cli 0.5.6__tar.gz → 0.5.7__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 (321) hide show
  1. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ARCHITECTURE.md +70 -0
  2. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/CHANGELOG.md +53 -0
  3. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/PKG-INFO +3 -2
  4. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/README.md +2 -1
  5. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_version.py +1 -1
  6. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/machine.py +14 -0
  7. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/types.py +5 -0
  8. taskops_cli-0.5.7/src/taskops/gitwork/inhabited.py +105 -0
  9. taskops_cli-0.5.7/src/taskops/gitwork/reading.py +165 -0
  10. taskops_cli-0.5.7/src/taskops/gitwork/scan.py +196 -0
  11. taskops_cli-0.5.7/src/taskops/http/editor.py +188 -0
  12. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/gitdoor.py +2 -9
  13. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/handler.py +4 -12
  14. taskops_cli-0.5.7/src/taskops/http/health.py +27 -0
  15. taskops_cli-0.5.7/src/taskops/http/listing.py +39 -0
  16. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/routes.py +14 -0
  17. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/server.py +5 -0
  18. taskops_cli-0.5.7/src/taskops/http/treewatch.py +107 -0
  19. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/boardview.py +5 -1
  20. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/brief.py +2 -1
  21. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/dossier.py +4 -0
  22. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/schema.py +8 -0
  23. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/server.py +2 -1
  24. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/tools.py +2 -2
  25. taskops_cli-0.5.7/src/taskops/ui/app.js +70 -0
  26. taskops_cli-0.5.7/src/taskops/ui/style.css +1 -0
  27. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_rows.py +2 -0
  28. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/update.py +27 -2
  29. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_core.py +25 -0
  30. taskops_cli-0.5.7/tests/test_editor.py +492 -0
  31. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_mcp.py +17 -0
  32. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_ui.py +102 -1
  33. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_verbs.py +74 -0
  34. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/fixture.json +325 -120
  35. taskops_cli-0.5.7/ui/smoke/sections/board-flip-abandoned-play.tsx +252 -0
  36. taskops_cli-0.5.7/ui/smoke/sections/card-progress.tsx +97 -0
  37. taskops_cli-0.5.7/ui/smoke/sections/editor-page.tsx +521 -0
  38. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/report-sandbox.tsx +11 -4
  39. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/section.ts +26 -1
  40. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/App.tsx +45 -6
  41. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/client.ts +21 -6
  42. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/board/CardTile.tsx +58 -0
  43. taskops_cli-0.5.7/ui/src/components/board/flip.ts +281 -0
  44. taskops_cli-0.5.7/ui/src/components/board/useFlip.ts +71 -0
  45. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/Drawer.tsx +39 -3
  46. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/Sections.tsx +27 -10
  47. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/chrome/Header.tsx +5 -1
  48. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/chrome/TabNav.tsx +12 -7
  49. taskops_cli-0.5.7/ui/src/components/editor/CodeView.tsx +212 -0
  50. taskops_cli-0.5.7/ui/src/components/editor/FileTree.tsx +344 -0
  51. taskops_cli-0.5.7/ui/src/components/editor/Icon.tsx +96 -0
  52. taskops_cli-0.5.7/ui/src/components/editor/QuickOpen.tsx +150 -0
  53. taskops_cli-0.5.7/ui/src/components/editor/Tabs.tsx +150 -0
  54. taskops_cli-0.5.7/ui/src/components/editor/fuzzy.ts +113 -0
  55. taskops_cli-0.5.7/ui/src/components/editor/highlight.ts +479 -0
  56. taskops_cli-0.5.7/ui/src/components/editor/icons.ts +89 -0
  57. taskops_cli-0.5.7/ui/src/components/editor/tree.ts +194 -0
  58. taskops_cli-0.5.7/ui/src/components/editor/useWorktree.ts +306 -0
  59. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/panels.ts +32 -0
  60. taskops_cli-0.5.7/ui/src/pages/Editor.tsx +566 -0
  61. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/pages/WorktreeDiff.tsx +16 -0
  62. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/pages/Worktrees.tsx +32 -1
  63. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/theme/tokens.css +72 -0
  64. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/types.ts +121 -0
  65. taskops_cli-0.5.6/src/taskops/ui/app.js +0 -61
  66. taskops_cli-0.5.6/src/taskops/ui/style.css +0 -1
  67. taskops_cli-0.5.6/ui/src/components/board/flip.ts +0 -105
  68. taskops_cli-0.5.6/ui/src/components/board/useFlip.ts +0 -96
  69. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.claude/settings.json +0 -0
  70. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.gitattributes +0 -0
  71. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.github/workflows/ci.yml +0 -0
  72. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.github/workflows/workflow.yml +0 -0
  73. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.gitignore +0 -0
  74. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.mcp.json +0 -0
  75. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/cache.sqlite +0 -0
  76. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/cache.sqlite-shm +0 -0
  77. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/cache.sqlite-wal +0 -0
  78. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/events.jsonl +0 -0
  79. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/live.sqlite +0 -0
  80. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/live.sqlite-shm +0 -0
  81. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board/live.sqlite-wal +0 -0
  82. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/board.json +0 -0
  83. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/reports/lifecycle-chapter.html +0 -0
  84. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/reports/reports-chapter.html +0 -0
  85. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/.taskops/ui.lock +0 -0
  86. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/CLAUDE.md +0 -0
  87. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/pyproject.toml +0 -0
  88. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/smoke.sh +0 -0
  89. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/__init__.py +0 -0
  90. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_clock.py +0 -0
  91. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_errors.py +0 -0
  92. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_ids.py +0 -0
  93. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_json.py +0 -0
  94. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_locate.py +0 -0
  95. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/_wire.py +0 -0
  96. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/board.py +0 -0
  97. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/__init__.py +0 -0
  98. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/__main__.py +0 -0
  99. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/admin.py +0 -0
  100. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/claude.py +0 -0
  101. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/commands.py +0 -0
  102. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/enrol.py +0 -0
  103. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/github.py +0 -0
  104. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/gitremote.py +0 -0
  105. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/grants.py +0 -0
  106. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/hooks.py +0 -0
  107. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/main.py +0 -0
  108. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/operate.py +0 -0
  109. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/paging.py +0 -0
  110. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/parser.py +0 -0
  111. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/pull.py +0 -0
  112. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/push.py +0 -0
  113. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/remote.py +0 -0
  114. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/rm.py +0 -0
  115. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/serving.py +0 -0
  116. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/team.py +0 -0
  117. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/watch.py +0 -0
  118. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/window.py +0 -0
  119. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/cli/wording.py +0 -0
  120. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/__init__.py +0 -0
  121. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/actors.py +0 -0
  122. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/challenge.py +0 -0
  123. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/chapters.py +0 -0
  124. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/event.py +0 -0
  125. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/forge.py +0 -0
  126. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/graph.py +0 -0
  127. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/holding.py +0 -0
  128. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/hours.py +0 -0
  129. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/kinds.py +0 -0
  130. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/mentions.py +0 -0
  131. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/replay.py +0 -0
  132. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/reports.py +0 -0
  133. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/review.py +0 -0
  134. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/scope.py +0 -0
  135. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/core/seams.py +0 -0
  136. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/__init__.py +0 -0
  137. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/bare.py +0 -0
  138. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/bind.py +0 -0
  139. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/catchup.py +0 -0
  140. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/claudefiles.py +0 -0
  141. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/diff.py +0 -0
  142. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/install.py +0 -0
  143. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/landing.py +0 -0
  144. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/onward.py +0 -0
  145. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/patch.py +0 -0
  146. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/remote.py +0 -0
  147. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/run.py +0 -0
  148. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/sig.py +0 -0
  149. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/trailer.py +0 -0
  150. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/gitwork/trees.py +0 -0
  151. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/__init__.py +0 -0
  152. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/admin.py +0 -0
  153. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/auth.py +0 -0
  154. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/feed.py +0 -0
  155. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/gitbody.py +0 -0
  156. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/gitpack.py +0 -0
  157. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/grants.py +0 -0
  158. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/ingest.py +0 -0
  159. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/login.py +0 -0
  160. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/members.py +0 -0
  161. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/mounts.py +0 -0
  162. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/page.py +0 -0
  163. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/removal.py +0 -0
  164. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/repos.py +0 -0
  165. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/rpc.py +0 -0
  166. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/scoped.py +0 -0
  167. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/stale.py +0 -0
  168. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/static.py +0 -0
  169. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/upstream.py +0 -0
  170. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/http/watcher.py +0 -0
  171. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/identity.py +0 -0
  172. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/__init__.py +0 -0
  173. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/__main__.py +0 -0
  174. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/activity.py +0 -0
  175. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/before.py +0 -0
  176. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/boards.py +0 -0
  177. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/chapter.py +0 -0
  178. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/fields.py +0 -0
  179. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/gitmoves.py +0 -0
  180. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/hello.py +0 -0
  181. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/integrate.py +0 -0
  182. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/orders.py +0 -0
  183. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/render.py +0 -0
  184. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/mcp/thread.py +0 -0
  185. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/py.typed +0 -0
  186. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/session.py +0 -0
  187. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/__init__.py +0 -0
  188. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/cache.py +0 -0
  189. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/creds.py +0 -0
  190. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/handover.py +0 -0
  191. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/live.py +0 -0
  192. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/log.py +0 -0
  193. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/mirroring.py +0 -0
  194. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/pubkeys.py +0 -0
  195. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/reviews.py +0 -0
  196. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/server.py +0 -0
  197. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/store/stores.py +0 -0
  198. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/ui/index.html +0 -0
  199. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/__init__.py +0 -0
  200. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_args.py +0 -0
  201. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_cards.py +0 -0
  202. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_chapter.py +0 -0
  203. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_context.py +0 -0
  204. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_facts.py +0 -0
  205. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_mentions.py +0 -0
  206. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_stories.py +0 -0
  207. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_waiting.py +0 -0
  208. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/_windows.py +0 -0
  209. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/activity.py +0 -0
  210. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/assign.py +0 -0
  211. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/card.py +0 -0
  212. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/events.py +0 -0
  213. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/filed.py +0 -0
  214. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/plan.py +0 -0
  215. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/project.py +0 -0
  216. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/pulse.py +0 -0
  217. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/record.py +0 -0
  218. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/report.py +0 -0
  219. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/review.py +0 -0
  220. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/src/taskops/verbs/take.py +0 -0
  221. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/__init__.py +0 -0
  222. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/conftest.py +0 -0
  223. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_architecture.py +0 -0
  224. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_claude.py +0 -0
  225. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_git.py +0 -0
  226. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_mirror.py +0 -0
  227. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_report.py +0 -0
  228. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_store.py +0 -0
  229. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_topology.py +0 -0
  230. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/test_window.py +0 -0
  231. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/tests/ui_harness.js +0 -0
  232. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/build.mjs +0 -0
  233. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/index.html +0 -0
  234. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/package-lock.json +0 -0
  235. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/package.json +0 -0
  236. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/main.tsx +0 -0
  237. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/run.mjs +0 -0
  238. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/actors-window-filter.tsx +0 -0
  239. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/actors.tsx +0 -0
  240. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/all-chapters-scope.tsx +0 -0
  241. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/board-columns.tsx +0 -0
  242. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/board-flow-view.tsx +0 -0
  243. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
  244. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/board-root-mount.tsx +0 -0
  245. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/card-diff-range.tsx +0 -0
  246. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
  247. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/chapter-landed.tsx +0 -0
  248. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
  249. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/chapters-open.tsx +0 -0
  250. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/comment-box.tsx +0 -0
  251. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
  252. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
  253. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/daysheet-midnight-spill.tsx +0 -0
  254. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/diff-page.tsx +0 -0
  255. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/dossier.tsx +0 -0
  256. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/event-stream.tsx +0 -0
  257. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/feed-reconnect.tsx +0 -0
  258. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/forge-links.tsx +0 -0
  259. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
  260. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/git-diff.tsx +0 -0
  261. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/hosted-window.tsx +0 -0
  262. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/monitor-panes.tsx +0 -0
  263. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/overlay-escape.tsx +0 -0
  264. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/report-markdown.tsx +0 -0
  265. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/reviewing-skew.tsx +0 -0
  266. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/swarm-topology.tsx +0 -0
  267. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/thread-closing-note.tsx +0 -0
  268. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/worktree-page-range.tsx +0 -0
  269. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections/worktrees-index.tsx +0 -0
  270. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/smoke/sections.mjs +0 -0
  271. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/completed.ts +0 -0
  272. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/actors/Daysheet.tsx +0 -0
  273. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/actors/DevPanel.tsx +0 -0
  274. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/board/Column.tsx +0 -0
  275. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/board/ViewToggle.tsx +0 -0
  276. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/CommentBox.tsx +0 -0
  277. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/Patch.tsx +0 -0
  278. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/Thread.tsx +0 -0
  279. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/split.ts +0 -0
  280. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/card/tokens.ts +0 -0
  281. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/chrome/AvatarStack.tsx +0 -0
  282. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/chrome/KpiRail.tsx +0 -0
  283. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
  284. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/flow/FlowView.tsx +0 -0
  285. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/flow/layout.ts +0 -0
  286. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/Chapter.tsx +0 -0
  287. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/DependencyChain.tsx +0 -0
  288. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/EditSurface.tsx +0 -0
  289. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/EventStream.tsx +0 -0
  290. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
  291. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/LiveLeases.tsx +0 -0
  292. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/Mentions.tsx +0 -0
  293. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/Pane.tsx +0 -0
  294. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/Swarm.tsx +0 -0
  295. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/monitor/Throughput.tsx +0 -0
  296. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/reports/ReportFrame.tsx +0 -0
  297. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/shared/Avatar.tsx +0 -0
  298. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/shared/Markdown.tsx +0 -0
  299. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/shared/Overlay.tsx +0 -0
  300. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/shared/Segmented.tsx +0 -0
  301. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/shared/overlayStack.ts +0 -0
  302. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/story/ChapterStory.tsx +0 -0
  303. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/story/stats.ts +0 -0
  304. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/toasts/ToastStack.tsx +0 -0
  305. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/toasts/model.ts +0 -0
  306. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/components/toasts/useToasts.ts +0 -0
  307. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/format.ts +0 -0
  308. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/hoursWindow.ts +0 -0
  309. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/links.tsx +0 -0
  310. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/main.tsx +0 -0
  311. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/markdown.ts +0 -0
  312. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/pages/Actors.tsx +0 -0
  313. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/pages/Board.tsx +0 -0
  314. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/pages/Monitor.tsx +0 -0
  315. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/pages/Reports.tsx +0 -0
  316. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/theme/theme.ts +0 -0
  317. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/useBoard.ts +0 -0
  318. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/src/useEvents.ts +0 -0
  319. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/styles/index.css +0 -0
  320. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/ui/tsconfig.json +0 -0
  321. {taskops_cli-0.5.6 → taskops_cli-0.5.7}/uv.lock +0 -0
@@ -107,6 +107,7 @@ erDiagram
107
107
  list files "edit surface, a hint, never a lock"
108
108
  list labels
109
109
  string assignee "who it is FOR — not a claim"
110
+ int progress "0-100, the worker's own REPORT — absent until one is made"
110
111
  string created_by
111
112
  float created
112
113
  float updated "replay arbiter, newer-wins"
@@ -183,6 +184,18 @@ turns it on derives exactly as it did before the feature existed
183
184
  (`tests/test_core.py::test_a_card_without_review_derives_exactly_as_before`). There is deliberately no path in
184
185
  this diagram labelled "recover": nothing is ever wrong for a recover to fix.
185
186
 
187
+ `progress` (0–100) is the one field that is neither: STORED, like the three
188
+ statuses, and yet no state at all — nothing derives from it, nothing gates on
189
+ it, `done` does not write 100, and a card nobody has reported on has no key
190
+ rather than a zero. It is a worker's own sentence about its own card
191
+ (`verbs/update.py::_progress`), written with `taskops_update progress=` every
192
+ 5–10 points, guarded exactly as a release is (yours, and never a stranger's).
193
+ It is not a derived state because deriving it would mean the board guessing how
194
+ far along somebody is; it is not a lifecycle status because nothing may ever
195
+ branch on it. What it buys beyond the number is the heartbeat: the write is MCP
196
+ traffic, and MCP traffic is the lease's only proof of life (§12), so a worker
197
+ that reports is a worker that cannot read as `stalled` while it is reporting.
198
+
186
199
  One more fact is derived the same way and is about a *reader*, not a card: a
187
200
  **pending mention** (`core/mentions.py`). Being named in a comment's optional
188
201
  `mentions[]` is pending until that actor writes any event on the card, or the
@@ -3042,3 +3055,60 @@ many slots and Actors anchors on a calendar month, so one number cannot be both,
3042
3055
  and moving between those tabs costs one request. A board one version behind
3043
3056
  sends no `window` key at all, and `windowSaid()` falls back to the day-bucket
3044
3057
  sentence — a degradation, never a blank.
3058
+
3059
+ ---
3060
+
3061
+ ## 22. The Editor — a worktree's files, live off the disk (2026-09-06)
3062
+
3063
+ Berna asked for "an editor inside taskops: the worktrees, a sidebar with the
3064
+ file directory, the code on the right in tabs, highlighted — the real code of
3065
+ each worktree in real time, from local". It is the sixth tab
3066
+ (`ui/src/pages/Editor.tsx`), read-only by construction, and it answers the one
3067
+ question the Worktrees page cannot: not what a branch ADDS as a patch, but what
3068
+ the code LOOKS LIKE right now in the directory a worker is sitting in.
3069
+
3070
+ **The contract** (`http/editor.py`, same envelope and same token door as `/git`):
3071
+
3072
+ ```
3073
+ GET /<board>/editor/trees every inhabited directory, the checkout (`main`) first
3074
+ GET /<board>/editor/tree?tree=<name> files: path, size, mtime, git state · capped, total, cap, seq
3075
+ GET /<board>/editor/file?tree=&path=[&base=] text or binary, truncated+cap, tracked, base {ref, sha}, marks [[from,to,kind]]
3076
+ GET /<board>/editor/diff?tree=&path=[&base=] the same file as a unified patch — the Worktrees page's own renderer draws it
3077
+ GET /<board>/editor/feed?tree=<name> WebSocket/SSE: {"type":"change","tree","seq"} — a SIGNAL, never the files
3078
+ ```
3079
+
3080
+ **Only a WINDOW answers.** The files live on the disk `taskops ui` runs on; a
3081
+ serve-mode host (taskops.bernardocastro.dev) holds boards and at most a bare
3082
+ `repo.git`, so it refuses with ONE sentence naming `taskops ui`
3083
+ (`editor.NO_CHECKOUT`) and the page quotes it — never an empty tree.
3084
+
3085
+ **The security line** is `gitwork/inhabited.py`, three sentences: a tree is a
3086
+ directory NAME matched before it is joined to anything; a file is resolved on
3087
+ both ends and refused — never repaired — when it leaves the tree, names `.git`,
3088
+ or is a symlink out; and a base ref passes `diff.usable`, the wall every ref of
3089
+ the /git door passes. `tests/test_editor.py` drives every shape through the door.
3090
+
3091
+ **Live, without a watcher dependency** (§11 stands): `gitwork/scan.py` lists a
3092
+ tree with git (index + untracked walk honouring every `.gitignore`, the
3093
+ directories nobody reads pruned at the directory) and measures it with one
3094
+ `stat` per file; `http/treewatch.py` runs that once a second for a tree
3095
+ somebody is listening to — `watcher.py`'s lifetime rule — and publishes when
3096
+ two scans differ. The page re-reads the listing on a signal and re-reads only
3097
+ the open tabs whose mtime moved, lighting the lines `changedSpan` names and
3098
+ keeping the scroll (`components/editor/useWorktree.ts`). Measured on a
3099
+ worker's tree in pinecall/v2: a new file in the tree 0.8 s after the write, an
3100
+ open tab re-rendered 0.2 s after it.
3101
+
3102
+ **The caps, all stated in the answer:** `scan.FILE_CAP` = 4000 files per tree
3103
+ (`capped` + `total` beside the list), `reading.CAP` = `patch.CAP` = 512 KiB per
3104
+ file and per patch (`truncated` + `cap`), binary decided on the first 8 KiB
3105
+ (a NUL or non-UTF-8) and never decoded. The gutter's base is the card's own
3106
+ chapter branch through `diff.compare_range` — the merge-base in flight, the
3107
+ fork point once integrated — and `HEAD` for the checkout.
3108
+
3109
+ **Highlighting is a compact tokenizer, not a dependency**
3110
+ (`components/editor/highlight.ts`, ~4 kB minified against ~25–45 kB for Prism
3111
+ or highlight.js with the eleven grammars): keyword, string, comment, number,
3112
+ tag, attribute — enough to read by, in six `--code-*` tokens that live in
3113
+ `theme/tokens.css` beside the palette rather than borrowing its status colours.
3114
+ The bundle grew 288 307 → 313 294 bytes for the whole feature.
@@ -3,6 +3,59 @@
3
3
  The source of truth for release notes — GitHub Releases are extracted from
4
4
  here, never written twice.
5
5
 
6
+ ## 0.5.7 — the Editor, and a card that says how far along it is
7
+
8
+ - **A card reports its own progress** — `taskops_update progress=0-100`, an
9
+ argument on the verb that already changes the card and deliberately not a
10
+ twelfth tool. Stored as an `edited` event, so the log, the fold and the
11
+ thread renderer read it with no new kind and no new replay rule. A REPORT and
12
+ never a state (ARCHITECTURE §3): nothing derives from it, nothing gates on
13
+ it, `done` does not write 100, and a card nobody has spoken about carries NO
14
+ key rather than a zero — the tile draws a bar only once somebody has spoken.
15
+ Guarded exactly as a release is: yours, never a stranger's, refused on a
16
+ closed card, and backwards is allowed because a task that turned out bigger
17
+ is honest. The cadence rides where a worker actually reads it — the assign
18
+ brief, the take's footer and the MCP protocol text — and the write doubles as
19
+ the lease's heartbeat, so a worker that reports cannot read as STALLED while
20
+ it is reporting. On screen: a track and its number in the tile, one pill in
21
+ the drawer, `· 35%` on the board's text line.
22
+ - **The board stopped going grey when you switch chapter.** Every tile arrived
23
+ inverted and the play that reveals them never ran, so the columns drew their
24
+ counts over nothing. Three defects, each found only after the one above it
25
+ was fixed: a cancelled play was dropped with its styles instead of being
26
+ played; rects were measured WITH the animation's own transforms, so the
27
+ offsets cascaded (6px → -12px → -6.66px → 12.99px); and the release looked
28
+ ids up in a ref map React had already emptied, since children detach in the
29
+ mutation phase before a parent's cleanup runs. All three now decided in
30
+ `components/board/flip.ts`, where a headless test can reach them, and
31
+ verified in a browser on the only shape that reproduces it — two chapters
32
+ that both have tiles: 0 stuck rounds of 6, where the same probe read 5 of 6.
33
+
34
+ - **A sixth tab, Editor** (`ui/src/pages/Editor.tsx`): pick a worktree — the
35
+ checkout first, then every directory under `.taskops/trees/` — and read its
36
+ files as an editor draws them: a folder tree on the left with each file's git
37
+ state (M/A/S/U) and a dot on every folder something moved under, tabs on the
38
+ right, line numbers, syntax colour for Python, TS/JS, JSON, YAML, TOML,
39
+ Markdown, SQL, bash, HTML and CSS, a gutter mark on every line that changed
40
+ since the branch base, and a per-file "diff vs base" toggle through the same
41
+ patch renderer the Worktrees page uses. Read-only, always.
42
+ - **Live from local disk**: a tree somebody is looking at is scanned once a
43
+ second (git for the listing, `stat` for the measure — no watcher dependency),
44
+ the page re-reads only the open tabs that moved, lights the changed lines and
45
+ keeps the scroll; a "last change: <file> · <ago>" line says the project is
46
+ moving. Doors in from every worktree row, the diff page and the dossier.
47
+ - **The tree folds to the working set** — what the branch wrote since its
48
+ base (`committed`, `deleted`, one `git diff --name-status` per scan) plus
49
+ the dirt — every other folder closed; ⌘P/Ctrl+P quick open with VS Code's
50
+ fuzzy rule (one definition, also the tree's filter); an Explorer-shaped
51
+ sidebar with keyboard navigation, file icons from one inline sprite, a tab
52
+ strip that scrolls on its own axis, and the whole page at viewport height.
53
+ - **`GET /<board>/editor/{trees,tree,file,diff,feed}`** (`http/editor.py`),
54
+ same token door and envelope as `/git`, served ONLY by a window — the
55
+ deployed host says so in one sentence. Files come only from inside the
56
+ board's own worktrees; `..`, absolute paths, `.git` and symlinks out are
57
+ refused. Caps stated in every answer. ARCHITECTURE.md §22.
58
+
6
59
  ## 0.5.6 — the hours page draws every hour of a day that opened before midnight
7
60
 
8
61
  Reported live on `/convo/`: the hours breakdown was empty. Monday said
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: taskops-cli
3
- Version: 0.5.6
3
+ Version: 0.5.7
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
@@ -412,7 +412,8 @@ taskops_merge integrate DONE cards into the milestone branch (--no-ff);
412
412
  milestone= lands a finished chapter. main is never touched
413
413
  taskops_take claim your card and get everything back
414
414
  taskops_update change the CARD: close, hand in for review, hand back, drop,
415
- retitle, re-spec, re-prioritise, declare a dependency
415
+ report progress (progress=0-100, every 5-10 points as you
416
+ work), retitle, re-spec, re-prioritise, declare a dependency
416
417
  taskops_review the verifier's one door: claim a submitted card, then
417
418
  verdict=pass|changes note=…
418
419
  taskops_comment say something on ANY card, including one somebody else holds
@@ -400,7 +400,8 @@ taskops_merge integrate DONE cards into the milestone branch (--no-ff);
400
400
  milestone= lands a finished chapter. main is never touched
401
401
  taskops_take claim your card and get everything back
402
402
  taskops_update change the CARD: close, hand in for review, hand back, drop,
403
- retitle, re-spec, re-prioritise, declare a dependency
403
+ report progress (progress=0-100, every 5-10 points as you
404
+ work), retitle, re-spec, re-prioritise, declare a dependency
404
405
  taskops_review the verifier's one door: claim a submitted card, then
405
406
  verdict=pass|changes note=…
406
407
  taskops_comment say something on ANY card, including one somebody else holds
@@ -1,4 +1,4 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  __title__ = "taskops"
4
- __version__ = "0.5.6" # single source of truth; pyproject reads it dynamically
4
+ __version__ = "0.5.7" # single source of truth; pyproject reads it dynamically
@@ -111,6 +111,20 @@ def check_release(card: Card, facts: Facts, actor: str, note: str) -> None:
111
111
  )
112
112
 
113
113
 
114
+ def check_progress(card: Card, facts: Facts, actor: str) -> None:
115
+ """A percentage is the worker's own report, so it is the worker's to write.
116
+
117
+ The same wall as closing and releasing (`_not_somebody_elses`): a lapsed
118
+ lease does not cost you your own card, a live holder who is somebody else
119
+ does. Nothing else is judged — not the direction (40 after 60 is honest when
120
+ the task turned out bigger) and not the step, because a guard on either
121
+ would only teach a worker to lie in fives.
122
+ """
123
+ if facts.status in CLOSED:
124
+ raise Refused(f"{card['id']} is {facts.status}; there is no progress left to report")
125
+ _not_somebody_elses(card, facts, actor)
126
+
127
+
114
128
  def _not_somebody_elses(card: Card, facts: Facts, actor: str) -> None:
115
129
  """A lease that lapsed does NOT cost you your own card.
116
130
 
@@ -85,6 +85,10 @@ class Card(TypedDict):
85
85
  files: list[str] # the edit surface as the planner understands it — a hint, never a lock
86
86
  labels: list[str] # routing and search; anybody may edit them
87
87
  assignee: str # "" is the open pool; dispatch writes it. NOT a claim — the lease is.
88
+ progress: NotRequired[int]
89
+ """0–100, the worker's own estimate, written by `taskops_update progress=`
90
+ and absent until the first report. A REPORT, never a state: nothing derives
91
+ from it and nothing gates on it (`verbs/update.py::_progress` argues it)."""
88
92
  created_by: str
89
93
  created: float
90
94
  updated: float # the replay arbiter (newer-wins)
@@ -189,5 +193,6 @@ EDITABLE = (
189
193
  "labels",
190
194
  "assignee",
191
195
  "review",
196
+ "progress",
192
197
  )
193
198
  LIST_FIELDS = ("after", "files", "labels", "criteria") # edited as a whole list, never appended to
@@ -0,0 +1,105 @@
1
+ """The inhabited directories of a checkout — the main worktree and every tree
2
+ under `.taskops/trees` — named, found, and walled.
3
+
4
+ `trees.py` is the GEOMETRY (where a card's directory goes, how it is cut);
5
+ this is the READ of that geometry for a screen: which directories exist right
6
+ now, and whether a path a browser sent stays inside one of them. The Editor
7
+ (ARCHITECTURE.md §22) shows the real files of a real worktree, so the walls
8
+ here are the whole of its security posture, in three sentences:
9
+
10
+ * A tree is addressed by its DIRECTORY NAME under `.taskops/trees/` — `tk-…`,
11
+ `_ms-…` — or by `main`, the checkout itself. `NAME` admits no slash and no
12
+ `..`, so a name never becomes a path before `locate()` has joined it to the
13
+ one directory it may name.
14
+ * A file is addressed by a plain repo-relative path, and `inside()` decides by
15
+ RESOLVING both ends: the real path of the file must sit under the real path
16
+ of the tree. A `..`, an absolute path, a doubled slash, a NUL, or a symlink
17
+ that leaves the tree all collapse to the same answer, `None` — a path is
18
+ refused, never repaired.
19
+ * `.git` is never a file of the project. In a linked worktree it is a FILE
20
+ pointing at the common dir, and the common dir holds every branch's objects
21
+ and the hooks; in the checkout it is the directory itself. Neither is code.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from typing import NamedTuple
28
+ from pathlib import Path
29
+
30
+ from . import run
31
+ from .trees import trees_dir
32
+
33
+ MAIN = "main"
34
+ """The checkout itself, listed first: the one directory that is not a card."""
35
+
36
+ NAME = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]{0,80}$")
37
+ """A tree's name — one directory name, never a path. `..` cannot match it."""
38
+
39
+
40
+ class Tree(NamedTuple):
41
+ name: str
42
+ path: Path
43
+ dir: str # repo-relative: "" for the checkout, `.taskops/trees/<name>` otherwise
44
+ branch: str # "" on a detached HEAD
45
+ head: str # the commit checked out, 40-hex, "" when git could not say
46
+
47
+
48
+ def listed(repo: Path) -> list[Tree]:
49
+ """Every inhabited directory, the checkout first and the trees in name order.
50
+
51
+ A directory under `trees/` with no `.git` in it is not a worktree — a stray
52
+ folder, a removed tree git has not pruned — and is left out rather than
53
+ shown as one: a tree on this screen is a place a worker can be."""
54
+ found = [describe(MAIN, repo, "")]
55
+ root = trees_dir(repo)
56
+ if root.is_dir():
57
+ for path in sorted(root.iterdir()):
58
+ if path.is_dir() and (path / ".git").exists():
59
+ found.append(describe(path.name, path, str(path.relative_to(repo))))
60
+ return found
61
+
62
+
63
+ def locate(repo: Path, name: str) -> Path | None:
64
+ """The directory `name` addresses, or None — the ONLY door from a string to
65
+ a tree. The name is matched BEFORE it is joined to anything."""
66
+ if name == MAIN:
67
+ return repo
68
+ if not NAME.match(name):
69
+ return None
70
+ path = trees_dir(repo) / name
71
+ return path if path.is_dir() and (path / ".git").exists() else None
72
+
73
+
74
+ def inside(tree: Path, rel: str) -> Path | None:
75
+ """The real file `rel` names inside `tree`, or None.
76
+
77
+ Resolved on both ends, so a symlink that points outside the tree is caught
78
+ by the same comparison that catches `..` — there is one wall, not a list of
79
+ shapes to recognise. A path that resolves to the tree itself is refused
80
+ too: the door reads FILES."""
81
+ if not rel or rel.startswith(("/", "\\")) or "\\" in rel or any(ord(c) < 32 for c in rel):
82
+ return None
83
+ parts = rel.split("/")
84
+ if any(part in ("", ".", "..", ".git") for part in parts):
85
+ return None
86
+ base = tree.resolve()
87
+ try:
88
+ target = (tree / rel).resolve(strict=True)
89
+ except OSError:
90
+ return None
91
+ if base not in target.parents:
92
+ return None
93
+ return target if target.is_file() else None
94
+
95
+
96
+ def describe(name: str, path: Path, rel: str) -> Tree:
97
+ """One inhabited directory, as the screen names it: two git questions."""
98
+ head = run.git("rev-parse", "--verify", "--quiet", "HEAD", cwd=path)
99
+ return Tree(
100
+ name=name,
101
+ path=path,
102
+ dir=rel,
103
+ branch=run.branch_at(path),
104
+ head=head.out.strip() if head.ok else "",
105
+ )
@@ -0,0 +1,165 @@
1
+ """One file of a worktree, read for the screen: text or "binary", capped, and
2
+ the lines that moved since the branch base — as gutter marks, or as a patch.
3
+
4
+ The WORKING COPY is the subject, not a commit: the Editor (ARCHITECTURE.md §22)
5
+ shows what a worker's directory holds right now, saved and unsaved-to-git
6
+ alike, which is exactly what `patch.show` (a committed file at a sha) cannot
7
+ say. So the bytes come off the disk, and "what changed" is `git diff <base> --
8
+ <path>` run INSIDE the tree, which git reads as "this commit against the
9
+ working copy as the index sees it". An untracked file is not in the index and
10
+ that diff is empty for it; `--no-index` against `/dev/null` is what git itself
11
+ offers for "a file that is all addition", and it is the one exception spelled
12
+ out here rather than a synthetic patch assembled by hand.
13
+
14
+ The BASE is a merge-base, never the ref itself, for the reason `diff.py`
15
+ argues at length: a card's chapter has moved on since the card branched, and
16
+ diffing against its tip would draw every sibling's work as this file's
17
+ change. No ref given means HEAD — "what did this directory change since its
18
+ last commit" — which is the right question for the checkout itself.
19
+
20
+ The file cap is `patch.CAP`, the one number every capped answer in this
21
+ package states, and a cut file SAYS it was cut. Binary is decided on the
22
+ first `PROBE` bytes: a NUL, or bytes that are not UTF-8, and the answer is
23
+ "binary, N bytes" with no text at all — never a screen of replacement glyphs.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from typing import NamedTuple
30
+ from pathlib import Path
31
+
32
+ from . import run, diff, patch
33
+
34
+ CAP = patch.CAP
35
+ PROBE = 8192
36
+
37
+ HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
38
+ """A hunk header: only the NEW side's start is read — every mark is a fact
39
+ about the file on screen — and the counts come from the body, because with
40
+ zero context git folds a changed line and the lines added right after it into
41
+ ONE hunk, and the header's two counts cannot say which is which."""
42
+
43
+ ADDED, MODIFIED, DELETED = "added", "modified", "deleted"
44
+
45
+
46
+ class Read(NamedTuple):
47
+ text: str
48
+ binary: bool
49
+ truncated: bool
50
+ size: int
51
+ mtime: float
52
+
53
+
54
+ class Base(NamedTuple):
55
+ ref: str # what was asked for: a branch name, or HEAD
56
+ sha: str # what was diffed against: the merge-base, 40-hex
57
+
58
+
59
+ class Mark(NamedTuple):
60
+ start: int # 1-based, inclusive, on the file as it stands
61
+ end: int
62
+ kind: str # ADDED | MODIFIED | DELETED (something was removed after `start`)
63
+
64
+
65
+ def read(path: Path, cap: int = CAP) -> Read:
66
+ """The bytes on disk, decided text or binary and capped. `path` has passed
67
+ `inhabited.inside` — this function trusts it and resolves nothing."""
68
+ found = path.stat()
69
+ with path.open("rb") as handle:
70
+ head = handle.read(cap + 1)
71
+ if _binary(head[:PROBE]):
72
+ return Read("", True, False, found.st_size, found.st_mtime)
73
+ cut = len(head) > cap
74
+ text = head[:cap].decode("utf-8", "ignore") if cut else head.decode("utf-8", "replace")
75
+ return Read(text, False, cut, found.st_size, found.st_mtime)
76
+
77
+
78
+ def base_of(tree: Path, ref: str) -> Base | None:
79
+ """The commit the working copy is compared against, or None when `ref`
80
+ names nothing this tree can see.
81
+
82
+ `diff.compare_range(ref, HEAD)` is the whole arithmetic, REUSED: the
83
+ merge-base while the card is in flight, and — once the card is integrated
84
+ and its tip is an ancestor of the chapter — the point it forked from, read
85
+ off the merge that brought it in. Seen live on a card the orchestrator had
86
+ just merged: `merge-base(ms/…, HEAD)` was HEAD itself, and every line the
87
+ card wrote read as unchanged. Both names pass `diff.resolve` inside it, the
88
+ package's one door from a string to a sha."""
89
+ if not ref:
90
+ head = diff.resolve(tree, "HEAD")
91
+ return Base("HEAD", head) if head else None
92
+ found = diff.compare_range(tree, ref, "HEAD")
93
+ return Base(ref, found[0]) if found else None
94
+
95
+
96
+ def tracked(tree: Path, rel: str) -> bool:
97
+ """Is this path in the index? `--error-unmatch` makes git say so by exit
98
+ code, which is the whole answer."""
99
+ return run.git("ls-files", "--error-unmatch", "--", rel, cwd=tree).ok
100
+
101
+
102
+ def marks(tree: Path, sha: str, rel: str, *, is_tracked: bool, lines: int) -> list[Mark]:
103
+ """Which lines of the file ON SCREEN differ from `sha`, from a zero-context
104
+ diff, paired POSITIONALLY inside each hunk the way `ui/src/components/card/
105
+ split.ts` pairs a patch: the first `+` lines opposite `-` lines are
106
+ modifications, the `+` lines past them are additions, and a hunk with no
107
+ `+` at all is a deletion after its anchor line."""
108
+ if not is_tracked:
109
+ return [Mark(1, lines, ADDED)] if lines else []
110
+ found: list[Mark] = []
111
+ opened, start, minus, plus = False, 0, 0, 0
112
+ for line in _diff(tree, sha, rel, context=0, is_tracked=True).splitlines():
113
+ hunk = HUNK.match(line)
114
+ if hunk is not None:
115
+ if opened:
116
+ found.extend(_marks_of(start, minus, plus))
117
+ opened, start, minus, plus = True, int(hunk.group(1)), 0, 0
118
+ elif opened and line.startswith("-"): # before the first `@@` the file
119
+ minus += 1 # headers `---`/`+++` are not lines of anybody's file
120
+ elif opened and line.startswith("+"):
121
+ plus += 1
122
+ if opened:
123
+ found.extend(_marks_of(start, minus, plus))
124
+ return found
125
+
126
+
127
+ def _marks_of(start: int, minus: int, plus: int) -> list[Mark]:
128
+ """One hunk's marks, positionally paired."""
129
+ if plus == 0:
130
+ anchor = max(start, 1) # `+0,0`: the deletion sits at the very top
131
+ return [Mark(anchor, anchor, DELETED)]
132
+ paired = min(minus, plus)
133
+ found = [Mark(start, start + paired - 1, MODIFIED)] if paired else []
134
+ if plus > paired:
135
+ found.append(Mark(start + paired, start + plus - 1, ADDED))
136
+ return found
137
+
138
+
139
+ def patch_of(tree: Path, sha: str, rel: str, *, is_tracked: bool, cap: int = CAP) -> tuple[str, bool]:
140
+ """(text, truncated) — the same unified patch the Worktrees page draws,
141
+ for one file of the working copy against `sha`."""
142
+ return patch.capped(_diff(tree, sha, rel, context=3, is_tracked=is_tracked), cap)
143
+
144
+
145
+ def _diff(tree: Path, sha: str, rel: str, *, context: int, is_tracked: bool) -> str:
146
+ """`git diff` exits 1 when the two sides differ — that is an answer, not a
147
+ failure, so both codes are read and anything else is an empty patch."""
148
+ if is_tracked:
149
+ raw = run.git("diff", "--no-color", f"-U{context}", sha, "--", rel, cwd=tree)
150
+ else:
151
+ raw = run.git("diff", "--no-color", f"-U{context}", "--no-index", "--", "/dev/null", rel, cwd=tree)
152
+ return raw.out if raw.code in (0, 1) else ""
153
+
154
+
155
+ def _binary(probe: bytes) -> bool:
156
+ """A NUL says binary outright. Bytes that are not UTF-8 say so too — unless
157
+ the failure sits in the last three bytes, where a codepoint the probe cut in
158
+ half looks exactly like one."""
159
+ if b"\0" in probe:
160
+ return True
161
+ try:
162
+ probe.decode("utf-8")
163
+ except UnicodeDecodeError as err:
164
+ return err.start < len(probe) - 3
165
+ return False