canopy-cli 3.1.0__tar.gz → 3.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/.github/workflows/ci.yml +0 -35
  2. canopy_cli-3.1.2/.github/workflows/release.yml +50 -0
  3. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/CHANGELOG.md +40 -0
  4. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/PKG-INFO +2 -2
  5. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/README.md +1 -1
  6. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/__init__.py +1 -1
  7. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/bootstrap.py +26 -2
  8. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/doctor.py +109 -1
  9. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/switch.py +37 -12
  10. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/cli/main.py +13 -25
  11. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/features/coordinator.py +18 -7
  12. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/git/repo.py +9 -0
  13. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/mcp/server.py +20 -8
  14. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/workspace/discovery.py +41 -0
  15. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_bootstrap.py +17 -0
  16. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_coordinator.py +47 -0
  17. canopy_cli-3.1.2/tests/test_discovery_worktrees.py +50 -0
  18. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_doctor.py +78 -0
  19. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_switch.py +92 -0
  20. canopy_cli-3.1.0/.github/workflows/publish.yml +0 -36
  21. canopy_cli-3.1.0/vscode-extension/.gitignore +0 -5
  22. canopy_cli-3.1.0/vscode-extension/.vscodeignore +0 -14
  23. canopy_cli-3.1.0/vscode-extension/CHANGELOG.md +0 -128
  24. canopy_cli-3.1.0/vscode-extension/LICENSE +0 -21
  25. canopy_cli-3.1.0/vscode-extension/README.md +0 -82
  26. canopy_cli-3.1.0/vscode-extension/esbuild.config.mjs +0 -71
  27. canopy_cli-3.1.0/vscode-extension/jest.config.ts +0 -22
  28. canopy_cli-3.1.0/vscode-extension/media/canopy-icon.png +0 -0
  29. canopy_cli-3.1.0/vscode-extension/media/canopy-icon.svg +0 -588
  30. canopy_cli-3.1.0/vscode-extension/media/screenshots/dashboard-feature.png +0 -0
  31. canopy_cli-3.1.0/vscode-extension/media/screenshots/dashboard-global.png +0 -0
  32. canopy_cli-3.1.0/vscode-extension/package-lock.json +0 -9054
  33. canopy_cli-3.1.0/vscode-extension/package.json +0 -297
  34. canopy_cli-3.1.0/vscode-extension/src/__mocks__/vscode.ts +0 -98
  35. canopy_cli-3.1.0/vscode-extension/src/canopyCli.test.ts +0 -196
  36. canopy_cli-3.1.0/vscode-extension/src/canopyCli.ts +0 -862
  37. canopy_cli-3.1.0/vscode-extension/src/canopyClient.ts +0 -704
  38. canopy_cli-3.1.0/vscode-extension/src/cliResolver.test.ts +0 -122
  39. canopy_cli-3.1.0/vscode-extension/src/cliResolver.ts +0 -134
  40. canopy_cli-3.1.0/vscode-extension/src/commands/createFeature.ts +0 -161
  41. canopy_cli-3.1.0/vscode-extension/src/commands/createFeatureFromIssue.ts +0 -85
  42. canopy_cli-3.1.0/vscode-extension/src/commands/installBackend.ts +0 -218
  43. canopy_cli-3.1.0/vscode-extension/src/commands/setupWizard.ts +0 -103
  44. canopy_cli-3.1.0/vscode-extension/src/extension.ts +0 -828
  45. canopy_cli-3.1.0/vscode-extension/src/mcpResolver.ts +0 -130
  46. canopy_cli-3.1.0/vscode-extension/src/stateReader.test.ts +0 -273
  47. canopy_cli-3.1.0/vscode-extension/src/stateReader.ts +0 -287
  48. canopy_cli-3.1.0/vscode-extension/src/statusBar.ts +0 -96
  49. canopy_cli-3.1.0/vscode-extension/src/types.ts +0 -192
  50. canopy_cli-3.1.0/vscode-extension/src/views/GlobalDashboardPanel.ts +0 -1020
  51. canopy_cli-3.1.0/vscode-extension/src/views/canopyTreeProvider.ts +0 -269
  52. canopy_cli-3.1.0/vscode-extension/src/views/themeShim.ts +0 -113
  53. canopy_cli-3.1.0/vscode-extension/src/watchers.ts +0 -79
  54. canopy_cli-3.1.0/vscode-extension/src/webview/cockpitPanel.ts +0 -395
  55. canopy_cli-3.1.0/vscode-extension/src/webview/components/branchLedger.ts +0 -72
  56. canopy_cli-3.1.0/vscode-extension/src/webview/components/bridge.ts +0 -72
  57. canopy_cli-3.1.0/vscode-extension/src/webview/components/capReachedModal.ts +0 -112
  58. canopy_cli-3.1.0/vscode-extension/src/webview/components/focusTile.ts +0 -139
  59. canopy_cli-3.1.0/vscode-extension/src/webview/components/newFeatureForm.ts +0 -174
  60. canopy_cli-3.1.0/vscode-extension/src/webview/components/styles.ts +0 -691
  61. canopy_cli-3.1.0/vscode-extension/src/webview/components/triageFeed.ts +0 -91
  62. canopy_cli-3.1.0/vscode-extension/src/webview/components/util.ts +0 -40
  63. canopy_cli-3.1.0/vscode-extension/src/webview/components/worktreeRow.ts +0 -71
  64. canopy_cli-3.1.0/vscode-extension/src/webview/dashboardPanel.ts +0 -904
  65. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/Dashboard.tsx +0 -137
  66. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/FeatureView.tsx +0 -972
  67. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/GlobalView.tsx +0 -649
  68. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/Skeletons.tsx +0 -38
  69. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/diff.ts +0 -118
  70. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/index.tsx +0 -17
  71. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/protocol.ts +0 -129
  72. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/vscode.ts +0 -30
  73. canopy_cli-3.1.0/vscode-extension/src/webview/newFeaturePanel.ts +0 -322
  74. canopy_cli-3.1.0/vscode-extension/src/webview/shared/pastel.css +0 -801
  75. canopy_cli-3.1.0/vscode-extension/src/webview/themes/index.ts +0 -42
  76. canopy_cli-3.1.0/vscode-extension/src/webview/themes/minimal.ts +0 -62
  77. canopy_cli-3.1.0/vscode-extension/src/webview/themes/navy.ts +0 -60
  78. canopy_cli-3.1.0/vscode-extension/src/webview/themes/render.ts +0 -96
  79. canopy_cli-3.1.0/vscode-extension/src/webview/themes/types.ts +0 -104
  80. canopy_cli-3.1.0/vscode-extension/tsconfig.json +0 -20
  81. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/.gitignore +0 -0
  82. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/AGENTS.md +0 -0
  83. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/CLAUDE.md +0 -0
  84. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/agents.md +0 -0
  85. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/architecture/providers.md +0 -0
  86. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/architecture.md +0 -0
  87. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/archive/test-plan.md +0 -0
  88. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/canopy-banner.svg +0 -0
  89. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-commit.svg +0 -0
  90. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-config.svg +0 -0
  91. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-done.svg +0 -0
  92. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-drift.svg +0 -0
  93. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-init.svg +0 -0
  94. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-list.svg +0 -0
  95. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-push.svg +0 -0
  96. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-review.svg +0 -0
  97. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-setup-agent.svg +0 -0
  98. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-stage.svg +0 -0
  99. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-state.svg +0 -0
  100. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-status.svg +0 -0
  101. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-switch.svg +0 -0
  102. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-triage.svg +0 -0
  103. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/cli-worktree.svg +0 -0
  104. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/commands.md +0 -0
  105. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/concepts.md +0 -0
  106. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/mcp.md +0 -0
  107. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/docs/workspace.md +0 -0
  108. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/gen_svgs.py +0 -0
  109. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/mockups/canopy-vscode.html +0 -0
  110. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/pyproject.toml +0 -0
  111. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/__init__.py +0 -0
  112. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/aliases.py +0 -0
  113. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/augments.py +0 -0
  114. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/bot_resolutions.py +0 -0
  115. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/bot_status.py +0 -0
  116. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/commit.py +0 -0
  117. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/conflicts.py +0 -0
  118. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/draft_replies.py +0 -0
  119. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/drift.py +0 -0
  120. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/errors.py +0 -0
  121. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/evacuate.py +0 -0
  122. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/feature_state.py +0 -0
  123. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/historian.py +0 -0
  124. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/ide_workspace.py +0 -0
  125. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/last_visit.py +0 -0
  126. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/migrate_slots.py +0 -0
  127. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/preflight_state.py +0 -0
  128. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/push.py +0 -0
  129. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/reads.py +0 -0
  130. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/resume.py +0 -0
  131. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/review_filter.py +0 -0
  132. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/ship.py +0 -0
  133. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/slot_details.py +0 -0
  134. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/slot_load.py +0 -0
  135. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/slots.py +0 -0
  136. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/stash.py +0 -0
  137. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/switch_preflight.py +0 -0
  138. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/thread_actions.py +0 -0
  139. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/thread_resolutions.py +0 -0
  140. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/actions/triage.py +0 -0
  141. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/agent/__init__.py +0 -0
  142. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/agent/runner.py +0 -0
  143. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/agent_setup/__init__.py +0 -0
  144. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/agent_setup/skills/augment-canopy/SKILL.md +0 -0
  145. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/agent_setup/skills/using-canopy/SKILL.md +0 -0
  146. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/cli/__init__.py +0 -0
  147. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/cli/render.py +0 -0
  148. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/cli/ui.py +0 -0
  149. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/features/__init__.py +0 -0
  150. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/git/__init__.py +0 -0
  151. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/git/hooks.py +0 -0
  152. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/git/multi.py +0 -0
  153. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/git/templates/post-checkout.py +0 -0
  154. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/graph/__init__.py +0 -0
  155. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/integrations/__init__.py +0 -0
  156. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/integrations/github.py +0 -0
  157. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/integrations/linear.py +0 -0
  158. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/integrations/precommit.py +0 -0
  159. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/mcp/__init__.py +0 -0
  160. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/mcp/client.py +0 -0
  161. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/providers/__init__.py +0 -0
  162. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/providers/github_issues.py +0 -0
  163. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/providers/linear.py +0 -0
  164. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/providers/types.py +0 -0
  165. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/workspace/__init__.py +0 -0
  166. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/workspace/config.py +0 -0
  167. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/workspace/context.py +0 -0
  168. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/src/canopy/workspace/workspace.py +0 -0
  169. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/__init__.py +0 -0
  170. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/conftest.py +0 -0
  171. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_action_errors.py +0 -0
  172. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_agent_setup.py +0 -0
  173. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_aliases.py +0 -0
  174. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_augments.py +0 -0
  175. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_bot_resolutions.py +0 -0
  176. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_bot_status.py +0 -0
  177. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_ci_status.py +0 -0
  178. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_commit.py +0 -0
  179. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_commit_push_integration.py +0 -0
  180. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_config.py +0 -0
  181. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_config_done.py +0 -0
  182. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_conflicts.py +0 -0
  183. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_context_stage.py +0 -0
  184. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_draft_replies.py +0 -0
  185. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_drift.py +0 -0
  186. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_evacuate.py +0 -0
  187. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_feature_state.py +0 -0
  188. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_feature_state_bot.py +0 -0
  189. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_historian.py +0 -0
  190. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_hooks.py +0 -0
  191. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_last_visit.py +0 -0
  192. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_linear_integration.py +0 -0
  193. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_mcp_server_worktree_create.py +0 -0
  194. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_migrate_slots.py +0 -0
  195. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_new_commands.py +0 -0
  196. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_precommit.py +0 -0
  197. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_providers_github_issues.py +0 -0
  198. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_providers_linear.py +0 -0
  199. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_providers_registry.py +0 -0
  200. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_providers_types.py +0 -0
  201. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_push.py +0 -0
  202. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_reads.py +0 -0
  203. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_repo.py +0 -0
  204. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_resume.py +0 -0
  205. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_review.py +0 -0
  206. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_review_filter.py +0 -0
  207. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_runner.py +0 -0
  208. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_ship.py +0 -0
  209. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_slot_details.py +0 -0
  210. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_slot_load.py +0 -0
  211. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_slots.py +0 -0
  212. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_stash_features.py +0 -0
  213. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_switch_action.py +0 -0
  214. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_switch_preflight.py +0 -0
  215. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_thread_actions.py +0 -0
  216. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_thread_graphql.py +0 -0
  217. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_triage.py +0 -0
  218. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_workspace.py +0 -0
  219. {canopy_cli-3.1.0 → canopy_cli-3.1.2}/tests/test_worktree_features.py +0 -0
@@ -39,38 +39,3 @@ jobs:
39
39
 
40
40
  - name: Run pytest
41
41
  run: pytest tests/ -v --maxfail=5
42
-
43
- extension-build:
44
- name: VSCode extension build
45
- runs-on: ubuntu-latest
46
- defaults:
47
- run:
48
- working-directory: vscode-extension
49
- steps:
50
- - uses: actions/checkout@v4
51
-
52
- - uses: actions/setup-node@v4
53
- with:
54
- node-version: "20"
55
- cache: npm
56
- cache-dependency-path: vscode-extension/package-lock.json
57
-
58
- - name: Install dependencies
59
- run: npm ci
60
-
61
- - name: Type-check
62
- run: npx tsc --noEmit
63
-
64
- - name: Bundle with esbuild
65
- run: npm run build
66
-
67
- - name: Package VSIX (sanity check)
68
- run: npx vsce package --allow-missing-repository
69
-
70
- - name: Upload VSIX artifact
71
- uses: actions/upload-artifact@v4
72
- with:
73
- name: canopy-vsix
74
- path: vscode-extension/*.vsix
75
- if-no-files-found: error
76
- retention-days: 14
@@ -0,0 +1,50 @@
1
+ name: Release (build + publish + tag)
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: write # push tag
8
+ id-token: write # OIDC for PyPI trusted publishing
9
+
10
+ jobs:
11
+ release:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+
22
+ - name: Read __version__
23
+ id: ver
24
+ run: |
25
+ V=$(python -c "import re,pathlib; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', pathlib.Path('src/canopy/__init__.py').read_text()).group(1))")
26
+ echo "version=$V" >> "$GITHUB_OUTPUT"
27
+ echo "Releasing v$V"
28
+
29
+ - name: Refuse if tag already exists
30
+ run: |
31
+ if git rev-parse -q --verify "refs/tags/v${{ steps.ver.outputs.version }}" >/dev/null; then
32
+ echo "::error::Tag v${{ steps.ver.outputs.version }} already exists. Bump __version__ first."
33
+ exit 1
34
+ fi
35
+
36
+ - name: Build sdist + wheel
37
+ run: |
38
+ python -m pip install --upgrade pip build twine
39
+ python -m build
40
+ python -m twine check dist/*
41
+
42
+ - name: Publish to PyPI
43
+ uses: pypa/gh-action-pypi-publish@release/v1
44
+
45
+ - name: Tag and push
46
+ run: |
47
+ git config user.name "github-actions[bot]"
48
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
49
+ git tag "v${{ steps.ver.outputs.version }}"
50
+ git push origin "v${{ steps.ver.outputs.version }}"
@@ -4,6 +4,46 @@ Tracks the Python side (CLI + MCP server). The VSCode extension has its own [vsc
4
4
 
5
5
  Versions follow semver. Pre-1.0 — minor bumps may add features or break behavior; the README is the source-of-truth contract.
6
6
 
7
+ ## 3.1.2 — 2026-07-04
8
+
9
+ Slot-model consistency fixes from canopy-test dogfooding.
10
+
11
+ ### Fixed
12
+ - `switch`: on the cold-Y fall-through (Y has a slots.json entry but this
13
+ repo's slot dir is missing), the vacating feature's slot is reclaimed for
14
+ the outgoing feature instead of allocating a fresh one — previously the
15
+ about-to-be-freed slot counted against the cap and raised a bogus
16
+ `no_free_slot`.
17
+ - `switch`: precondition failures raised before any git mutation
18
+ (`no_free_slot`, `unknown_slot`, `evict_to_occupied`,
19
+ `warm_worktree_dirty_on_promote`) no longer stamp the `in_flight` marker
20
+ when no repo has been touched — a clean no-op failure used to brick every
21
+ subsequent switch via `slot_state_inconsistent`.
22
+ - `doctor`: new `slot_repo_worktree_missing` check (+ auto-repair via
23
+ `git worktree prune` + `worktree add`) catches half-materialized slots
24
+ where a slot holds a feature but one repo's worktree is gone.
25
+ - `doctor`: the pre-3.0 `worktree_orphan` check now skips `worktree-N` slot
26
+ dirs — `doctor --fix` no longer deletes warm slots.
27
+ - `worktree_bootstrap`: resolves worktree paths via slots.json (the 3.0
28
+ source of truth) instead of the legacy `features.json` cache, which is
29
+ empty in 3.0 — bootstrap raised `no_worktrees` for every warm feature.
30
+ Falls back to the legacy cache for pre-3.0 workspaces.
31
+ - `canopy init` / `workspace_reinit`: existing worktrees are reported by
32
+ occupant feature (resolved via slots.json) instead of listing slot ids
33
+ (`worktree-N`) as if they were feature names.
34
+ - `coordinator.status()` / `feature_changes`: honor the per-repo `branches`
35
+ map (`lane.branch_for`) instead of assuming branch == feature name —
36
+ mismatched-naming features no longer mis-report as having no branch or no
37
+ changes.
38
+
39
+ ## 3.1.1 — 2026-05-31
40
+
41
+ ### Fixed
42
+ - `canopy-mcp --help` / `-h` now prints usage and exits instead of starting the
43
+ stdio server and crashing on an empty stdin read. The MCP entry point is not
44
+ meant to be invoked interactively; the help text says so and points at
45
+ `canopy setup-agent`.
46
+
7
47
  ## 3.1.0 — 2026-05-30 (Plan 2 — Feature Resume)
8
48
 
9
49
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canopy-cli
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: Workspace-first development orchestrator for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/ashmitb95/canopy
6
6
  Project-URL: Source, https://github.com/ashmitb95/canopy
@@ -254,7 +254,7 @@ canopy doctor # diagnose drift / staleness
254
254
  <img src="docs/cli-state.svg" alt="canopy state" width="720">
255
255
  </p>
256
256
 
257
- The CLI and MCP server are thin wrappers over the same actions — `canopy state X` and `mcp__canopy__feature_state(feature='X')` return identical bytes. There's also a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=SingularityInc.canopy) reading the same state the agent reads.
257
+ The CLI and MCP server are thin wrappers over the same actions — `canopy state X` and `mcp__canopy__feature_state(feature='X')` return identical bytes. There's also a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=SingularityInc.canopy) (source at [`ashmitb95/canopy-dashboard`](https://github.com/ashmitb95/canopy-dashboard)) reading the same state the agent reads.
258
258
 
259
259
  Full CLI reference: [docs/commands.md](docs/commands.md).
260
260
 
@@ -226,7 +226,7 @@ canopy doctor # diagnose drift / staleness
226
226
  <img src="docs/cli-state.svg" alt="canopy state" width="720">
227
227
  </p>
228
228
 
229
- The CLI and MCP server are thin wrappers over the same actions — `canopy state X` and `mcp__canopy__feature_state(feature='X')` return identical bytes. There's also a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=SingularityInc.canopy) reading the same state the agent reads.
229
+ The CLI and MCP server are thin wrappers over the same actions — `canopy state X` and `mcp__canopy__feature_state(feature='X')` return identical bytes. There's also a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=SingularityInc.canopy) (source at [`ashmitb95/canopy-dashboard`](https://github.com/ashmitb95/canopy-dashboard)) reading the same state the agent reads.
230
230
 
231
231
  Full CLI reference: [docs/commands.md](docs/commands.md).
232
232
 
@@ -1,2 +1,2 @@
1
1
  """Canopy — workspace-first development orchestrator."""
2
- __version__ = "3.1.0"
2
+ __version__ = "3.1.2"
@@ -63,7 +63,10 @@ def bootstrap_feature(
63
63
  if not worktree_paths:
64
64
  raise BlockerError(
65
65
  code="no_worktrees",
66
- what=f"feature '{feature_name}' has no worktree paths recorded",
66
+ what=(
67
+ f"feature '{feature_name}' is not warm in any slot "
68
+ f"(nothing to bootstrap) — `canopy switch {feature_name}` first"
69
+ ),
67
70
  )
68
71
 
69
72
  results: dict[str, dict[str, Any]] = {}
@@ -235,7 +238,28 @@ def _validate_steps(steps: Iterable[str] | None) -> set[str]:
235
238
  def _resolve_worktree_paths(
236
239
  workspace: Workspace, feature_name: str,
237
240
  ) -> dict[str, Path]:
238
- """Pull recorded worktree paths from features.json."""
241
+ """Resolve each repo's warm worktree dir for ``feature_name``.
242
+
243
+ Wave 3.0: the authoritative source is slots.json — a warm feature's
244
+ per-repo worktrees live under its slot at
245
+ ``.canopy/worktrees/worktree-N/<repo>``. Falls back to the legacy
246
+ ``features.json`` ``worktree_paths`` cache for pre-3.0 workspaces (no
247
+ slots.json). The old code read only the legacy cache, which is empty in
248
+ 3.0 — so bootstrap raised ``no_worktrees`` for every warm feature.
249
+ """
250
+ from . import slots as slots_mod
251
+ from .aliases import repos_for_feature
252
+
253
+ slot_id = slots_mod.slot_for_feature(workspace, feature_name)
254
+ if slot_id is not None:
255
+ out: dict[str, Path] = {}
256
+ for repo_name in repos_for_feature(workspace, feature_name):
257
+ p = slots_mod.slot_worktree_path(workspace, slot_id, repo_name)
258
+ if (p / ".git").exists():
259
+ out[repo_name] = p
260
+ return out
261
+
262
+ # Legacy pre-3.0 fallback: features.json worktree_paths cache.
239
263
  import json
240
264
  path = workspace.config.root / ".canopy" / "features.json"
241
265
  if not path.exists():
@@ -222,7 +222,14 @@ def check_active_feature_path_missing(workspace: Workspace) -> list[Issue]:
222
222
 
223
223
 
224
224
  def check_worktree_orphan(workspace: Workspace) -> list[Issue]:
225
- """Worktree directories under .canopy/worktrees/ not referenced by any feature."""
225
+ """Worktree directories under .canopy/worktrees/ not referenced by any feature.
226
+
227
+ Pre-3.0 layout only (``<feature>/<repo>``). The Wave-3.0 slot layout
228
+ (``worktree-N/<repo>``) is owned by the ``slot_*`` checks — skip those
229
+ dirs here, or this check would flag every warm slot as an orphan and
230
+ ``--fix`` would delete it.
231
+ """
232
+ import re
226
233
  wt_root = workspace.config.root / ".canopy" / "worktrees"
227
234
  if not wt_root.exists():
228
235
  return []
@@ -231,6 +238,8 @@ def check_worktree_orphan(workspace: Workspace) -> list[Issue]:
231
238
  for feat_dir in sorted(wt_root.iterdir()):
232
239
  if not feat_dir.is_dir():
233
240
  continue
241
+ if re.fullmatch(r"worktree-\d+", feat_dir.name):
242
+ continue # slot dir — handled by check_slot_* functions
234
243
  feature_name = feat_dir.name
235
244
  feature_data = features.get(feature_name)
236
245
  feature_repos = (feature_data or {}).get("repos") or []
@@ -604,6 +613,66 @@ def check_slot_branch_mismatches(workspace: Workspace) -> list[Issue]:
604
613
  return issues
605
614
 
606
615
 
616
+ def check_slot_repo_worktree_missing(workspace: Workspace) -> list[Issue]:
617
+ """A slot holds feature F, but one of F's repos has no worktree on disk.
618
+
619
+ This is the per-repo divergence the other slot checks can't see:
620
+ ``slot_entry_orphan`` only inspects the ``worktree-N/`` top dir (which
621
+ survives as long as ANY repo's subdir remains), and
622
+ ``slot_branch_mismatch`` ``continue``s past a non-existent per-repo path.
623
+ A half-materialized slot bricked canopy-test (``switch`` then tried to
624
+ allocate a fresh slot for an already-occupied feature → ``no_free_slot``).
625
+
626
+ Auto-fixable by recreating the worktree from the feature's branch —
627
+ unless the branch itself is gone, in which case ``branches_missing``
628
+ owns the deeper problem and this is advice-only.
629
+ """
630
+ from . import slots as slots_mod
631
+ from .aliases import repos_for_feature
632
+
633
+ state = slots_mod.read_state(workspace)
634
+ if state is None:
635
+ return []
636
+ issues: list[Issue] = []
637
+ for sid, entry in state.slots.items():
638
+ repo_branches = repos_for_feature(workspace, entry.feature) or {}
639
+ for repo_name, expected_branch in repo_branches.items():
640
+ slot_path = slots_mod.slot_worktree_path(workspace, sid, repo_name)
641
+ if (slot_path / ".git").exists():
642
+ continue
643
+ try:
644
+ rs = workspace.get_repo(repo_name)
645
+ except KeyError:
646
+ continue # features_unknown_repo owns this
647
+ branch_ok = rs.abs_path.exists() and git.branch_exists(
648
+ rs.abs_path, expected_branch,
649
+ )
650
+ issues.append(Issue(
651
+ code="slot_repo_worktree_missing",
652
+ severity="error",
653
+ what=(
654
+ f"slot '{sid}' is missing its '{repo_name}' worktree"
655
+ f" (feature '{entry.feature}', branch '{expected_branch}')"
656
+ ),
657
+ expected=str(slot_path),
658
+ actual="(no worktree on disk)",
659
+ repo=repo_name,
660
+ feature=entry.feature,
661
+ fix_action=(
662
+ f"recreate: git worktree add {slot_path} {expected_branch}"
663
+ if branch_ok else
664
+ f"branch '{expected_branch}' is gone in {repo_name} —"
665
+ f" restore it (see branches_missing) before recreating"
666
+ ),
667
+ auto_fixable=branch_ok,
668
+ details={
669
+ "slot": sid, "feature": entry.feature, "repo": repo_name,
670
+ "branch": expected_branch, "slot_path": str(slot_path),
671
+ },
672
+ ))
673
+ return issues
674
+
675
+
607
676
  # ── Install-staleness checks ─────────────────────────────────────────────
608
677
 
609
678
 
@@ -895,6 +964,7 @@ _CHECKS: dict[str, tuple[str, Any]] = {
895
964
  "vsix_duplicates": ("vsix", check_vsix_duplicates),
896
965
  "slot_dir_orphan": ("slots", check_slot_dir_orphans),
897
966
  "slot_entry_orphan": ("slots", check_slot_entry_orphans),
967
+ "slot_repo_worktree_missing": ("slots", check_slot_repo_worktree_missing),
898
968
  "slot_branch_mismatch": ("slots", check_slot_branch_mismatches),
899
969
  # slot_detached_head shares its check function with slot_branch_mismatch
900
970
  # (one walker emits both codes). The registry entry uses a sentinel
@@ -1276,6 +1346,43 @@ def repair_slot_entry_orphan(workspace: Workspace, issue: Issue) -> RepairResult
1276
1346
  action_taken=f"dropped slots.json entry for '{sid}'")
1277
1347
 
1278
1348
 
1349
+ def repair_slot_repo_worktree_missing(workspace: Workspace, issue: Issue) -> RepairResult:
1350
+ """Recreate the missing per-repo worktree from the feature's branch.
1351
+
1352
+ Restores the slot invariant rather than dropping the slot entry —
1353
+ dropping it would orphan the slot's surviving repos. Idempotent: a no-op
1354
+ if the worktree reappeared.
1355
+ """
1356
+ d = issue.details or {}
1357
+ repo_name, branch, slot_path_s = d.get("repo"), d.get("branch"), d.get("slot_path")
1358
+ if not (repo_name and branch and slot_path_s):
1359
+ return RepairResult(code=issue.code, success=False, action_taken="",
1360
+ error="missing repo/branch/slot_path on issue")
1361
+ slot_path = Path(slot_path_s)
1362
+ if (slot_path / ".git").exists():
1363
+ return RepairResult(code=issue.code, success=True, repo=repo_name,
1364
+ feature=issue.feature,
1365
+ action_taken="worktree already present")
1366
+ try:
1367
+ rs = workspace.get_repo(repo_name)
1368
+ except KeyError as e:
1369
+ return RepairResult(code=issue.code, success=False, action_taken="",
1370
+ error=str(e), repo=repo_name)
1371
+ repo_root = git.worktree_main_path(rs.abs_path) or rs.abs_path
1372
+ slot_path.parent.mkdir(parents=True, exist_ok=True)
1373
+ try:
1374
+ # Prune first so a stale registration for this path doesn't block add.
1375
+ git.worktree_prune(repo_root)
1376
+ git.worktree_add(repo_root, slot_path, branch, create_branch=False)
1377
+ except git.GitError as e:
1378
+ return RepairResult(code=issue.code, success=False, repo=repo_name,
1379
+ feature=issue.feature, action_taken="",
1380
+ error=str(e))
1381
+ return RepairResult(code=issue.code, success=True, repo=repo_name,
1382
+ feature=issue.feature,
1383
+ action_taken=f"git worktree add {slot_path} {branch}")
1384
+
1385
+
1279
1386
  _REPAIRS: dict[str, Any] = {
1280
1387
  "heads_stale": repair_heads_stale,
1281
1388
  "active_feature_orphan": repair_active_feature_orphan,
@@ -1291,6 +1398,7 @@ _REPAIRS: dict[str, Any] = {
1291
1398
  "mcp_orphans": repair_mcp_orphans,
1292
1399
  "vsix_duplicates": repair_vsix_duplicates,
1293
1400
  "slot_entry_orphan": repair_slot_entry_orphan,
1401
+ "slot_repo_worktree_missing": repair_slot_repo_worktree_missing,
1294
1402
  # cli_stale, mcp_stale, features_unknown_repo, branches_missing,
1295
1403
  # slot_dir_orphan, slot_branch_mismatch have no auto-fix —
1296
1404
  # repair returns surfaced advice via the issue's `fix_action` instead.
@@ -34,6 +34,17 @@ from .aliases import resolve_feature, repos_for_feature
34
34
  from .errors import BlockerError, FixAction
35
35
 
36
36
 
37
+ # Precondition BlockerError codes raised by _do_repo_switch BEFORE any git
38
+ # mutation in the failing repo. When one fires with no prior repo completed,
39
+ # the workspace is NOT partially flipped, so no in_flight marker is warranted.
40
+ _PRE_MUTATION_CODES = frozenset({
41
+ "no_free_slot",
42
+ "unknown_slot",
43
+ "evict_to_occupied",
44
+ "warm_worktree_dirty_on_promote",
45
+ })
46
+
47
+
37
48
  def switch(
38
49
  workspace: Workspace,
39
50
  feature: str | None = None,
@@ -158,15 +169,20 @@ def switch(
158
169
  evict_to=evict_to,
159
170
  )
160
171
  except BlockerError as e:
161
- # Even a structured precondition failure (e.g. dirty warm
162
- # worktree on the second repo) can leave disk partially
163
- # mutated by earlier repos. Persist an in_flight marker so
164
- # the next switch refuses to operate on a lie.
165
- _persist_in_flight(
166
- workspace, feature_name, previously_canonical,
167
- failed_repo=repo_name, error_what=e.what or str(e),
168
- completed_results=per_repo_results,
169
- )
172
+ # A structured precondition failure raised BEFORE any git
173
+ # mutation (no_free_slot, dirty warm worktree, bad --evict-to)
174
+ # leaves disk untouched in this repo. Only stamp in_flight when
175
+ # something is actually half-flipped: an earlier repo already
176
+ # completed (per_repo_results non-empty), OR this was a mid-op
177
+ # failure (not one of the pre-mutation precondition codes).
178
+ # Otherwise a clean no-op failure would brick every future
179
+ # switch via slot_state_inconsistent.
180
+ if per_repo_results or e.code not in _PRE_MUTATION_CODES:
181
+ _persist_in_flight(
182
+ workspace, feature_name, previously_canonical,
183
+ failed_repo=repo_name, error_what=e.what or str(e),
184
+ completed_results=per_repo_results,
185
+ )
170
186
  raise
171
187
  except Exception as e:
172
188
  # Mid-op failure with no rollback walker (yet). Surface enough
@@ -281,9 +297,14 @@ def _do_repo_switch(
281
297
  per_repo_results.append(result)
282
298
  return
283
299
  # Fall through: Y's slot entry exists but this repo's slot
284
- # dir is missing (partial-scope drift). Treat as cold-Y.
285
-
286
- # Cold-Y path: allocate a fresh slot for X
300
+ # dir is missing (orphaned worktree / partial-scope drift).
301
+ # Y is still being promoted to canonical, so it vacates its
302
+ # slot regardless X reclaims y_slot below instead of needing
303
+ # a brand-new allocation. Allocating blindly here would count
304
+ # Y's own about-to-be-freed slot against the cap and raise a
305
+ # bogus no_free_slot (the canopy-test billing-export lock-out).
306
+
307
+ # Cold-Y path: allocate (or reclaim) the slot X will occupy.
287
308
  state = slots_mod.read_state(workspace) or slots_mod.SlotState(
288
309
  slot_count=workspace.config.slots,
289
310
  )
@@ -304,6 +325,10 @@ def _do_repo_switch(
304
325
  details={"slot": evict_to, "occupant": existing},
305
326
  )
306
327
  x_slot = evict_to
328
+ elif y_slot is not None:
329
+ # Y already owns y_slot and is leaving it (becoming canonical);
330
+ # X reclaims it. _post_switch_persist re-stamps the slot to X.
331
+ x_slot = y_slot
307
332
  else:
308
333
  x_slot = slots_mod.allocate_slot(state)
309
334
  if x_slot is None:
@@ -118,15 +118,10 @@ def cmd_init(args: argparse.Namespace) -> None:
118
118
  if args.json:
119
119
  all_dirs = [d for d in root.iterdir() if d.is_dir() and not d.name.startswith(".")]
120
120
  skipped = [d.name for d in all_dirs if not (d / ".git").exists()]
121
- # Detect existing feature worktrees
122
- worktrees_dir = root / ".canopy" / "worktrees"
123
- active_worktrees = {}
124
- if worktrees_dir.is_dir():
125
- for feat_dir in worktrees_dir.iterdir():
126
- if feat_dir.is_dir():
127
- active_worktrees[feat_dir.name] = sorted(
128
- d.name for d in feat_dir.iterdir() if d.is_dir()
129
- )
121
+ # Detect existing worktrees, keyed by FEATURE (slot dirs resolve their
122
+ # occupant via slots.json not reported as if the slot id were a feature).
123
+ from ..workspace.discovery import summarize_worktree_dirs
124
+ active_worktrees = summarize_worktree_dirs(root)
130
125
  _print_json({
131
126
  "root": str(root),
132
127
  "repos": [{
@@ -203,22 +198,15 @@ def cmd_init(args: argparse.Namespace) -> None:
203
198
  console.print(f" mcp [muted]· {note}[/]")
204
199
  console.print(f" [muted]Restart Claude Code to pick up the skill + MCP. Skip with --no-agent.[/]")
205
200
 
206
- # Report existing feature worktrees under .canopy/
207
- canopy_dir = root / ".canopy"
208
- worktrees_dir = canopy_dir / "worktrees"
209
- if worktrees_dir.is_dir():
210
- features_with_wt = sorted(
211
- d.name for d in worktrees_dir.iterdir() if d.is_dir()
212
- )
213
- if features_with_wt:
214
- console.print()
215
- console.print(f" [header]Active worktrees ({len(features_with_wt)})[/]")
216
- for feat in features_with_wt:
217
- feat_dir = worktrees_dir / feat
218
- wt_repos = sorted(
219
- d.name for d in feat_dir.iterdir() if d.is_dir()
220
- )
221
- console.print(f" [feature]{feat}[/] [muted]{SYM_ARROW}[/] {', '.join(wt_repos)}")
201
+ # Report existing worktrees under .canopy/, keyed by feature (slot dirs
202
+ # resolve their occupant via slots.json — see summarize_worktree_dirs).
203
+ from ..workspace.discovery import summarize_worktree_dirs
204
+ worktrees = summarize_worktree_dirs(root)
205
+ if worktrees:
206
+ console.print()
207
+ console.print(f" [header]Active worktrees ({len(worktrees)})[/]")
208
+ for feat, wt_repos in sorted(worktrees.items()):
209
+ console.print(f" [feature]{feat}[/] [muted]{SYM_ARROW}[/] {', '.join(wt_repos)}")
222
210
  console.print()
223
211
 
224
212
 
@@ -314,6 +314,7 @@ class FeatureCoordinator:
314
314
  linear_issue=data.get("linear_issue", ""),
315
315
  linear_title=data.get("linear_title", ""),
316
316
  linear_url=data.get("linear_url", ""),
317
+ branches=dict(data.get("branches") or {}),
317
318
  )
318
319
  else:
319
320
  # Implicit feature
@@ -438,7 +439,10 @@ class FeatureCoordinator:
438
439
  continue
439
440
 
440
441
  try:
441
- changes = git.changed_files_with_status(scan_path, name, base)
442
+ # Per-repo branch override: scan the lane's actual branch
443
+ # for this repo, not the bare feature name.
444
+ branch = lane.branch_for(repo_name)
445
+ changes = git.changed_files_with_status(scan_path, branch, base)
442
446
  result[repo_name] = {
443
447
  "has_branch": True,
444
448
  "path": str(scan_path),
@@ -550,7 +554,12 @@ class FeatureCoordinator:
550
554
  continue
551
555
 
552
556
  base = state.config.default_branch
553
- has_branch = git.branch_exists(state.abs_path, lane.name)
557
+ # Honor per-repo branch overrides — the branch may differ from
558
+ # the feature name (FeatureLane.branches map). Using lane.name
559
+ # here would mis-report mismatched-naming features as having no
560
+ # branch / no changes.
561
+ branch = lane.branch_for(repo_name)
562
+ has_branch = git.branch_exists(state.abs_path, branch)
554
563
 
555
564
  if not has_branch:
556
565
  lane.repo_states[repo_name] = {
@@ -564,10 +573,10 @@ class FeatureCoordinator:
564
573
 
565
574
  try:
566
575
  ahead, behind = git.divergence(
567
- state.abs_path, lane.name, base
576
+ state.abs_path, branch, base
568
577
  )
569
- files = git.changed_files(state.abs_path, lane.name, base)
570
- dirty = state.is_dirty if state.current_branch == lane.name else False
578
+ files = git.changed_files(state.abs_path, branch, base)
579
+ dirty = state.is_dirty if state.current_branch == branch else False
571
580
 
572
581
  repo_state: dict = {
573
582
  "has_branch": True,
@@ -580,7 +589,7 @@ class FeatureCoordinator:
580
589
  }
581
590
 
582
591
  # Check if branch is checked out in a worktree
583
- wt_path = git.worktree_for_branch(state.abs_path, lane.name)
592
+ wt_path = git.worktree_for_branch(state.abs_path, branch)
584
593
  if wt_path:
585
594
  repo_state["worktree_path"] = wt_path
586
595
 
@@ -1220,8 +1229,10 @@ class FeatureCoordinator:
1220
1229
  try:
1221
1230
  repo_name = repo_dir.name
1222
1231
  state = self.workspace.get_repo(repo_name)
1232
+ # Per-repo branch override (else the feature name).
1233
+ branch = (meta.get("branches") or {}).get(repo_name, feat_name)
1223
1234
  ahead, _ = git.divergence(
1224
- repo_dir, feat_name, state.config.default_branch,
1235
+ repo_dir, branch, state.config.default_branch,
1225
1236
  )
1226
1237
  if ahead > 0:
1227
1238
  all_merged = False
@@ -732,6 +732,15 @@ def worktree_remove(repo_path: Path, worktree_path: Path, force: bool = False) -
732
732
  return _run(args, cwd=repo_path)
733
733
 
734
734
 
735
+ def worktree_prune(repo_path: Path) -> str:
736
+ """Prune worktree registrations whose directories are gone.
737
+
738
+ Clears stale ``.git/worktrees/<name>`` entries so a subsequent
739
+ ``worktree add`` at the same path isn't rejected as already-registered.
740
+ """
741
+ return _run(["worktree", "prune"], cwd=repo_path)
742
+
743
+
735
744
  def worktree_move(main_repo: Path, old_path: Path, new_path: Path) -> None:
736
745
  """Run `git worktree move <old_path> <new_path>` from main_repo.
737
746
 
@@ -1534,14 +1534,10 @@ def workspace_reinit(name: str | None = None, dry_run: bool = False) -> dict:
1534
1534
  d for d in root.iterdir() if d.is_dir() and not d.name.startswith(".")
1535
1535
  ]
1536
1536
  skipped = [d.name for d in all_dirs if not (d / ".git").exists()]
1537
- worktrees_dir = root / ".canopy" / "worktrees"
1538
- active_worktrees: dict[str, list[str]] = {}
1539
- if worktrees_dir.is_dir():
1540
- for feat_dir in worktrees_dir.iterdir():
1541
- if feat_dir.is_dir():
1542
- active_worktrees[feat_dir.name] = sorted(
1543
- d.name for d in feat_dir.iterdir() if d.is_dir()
1544
- )
1537
+ # Keyed by FEATURE slot dirs resolve their occupant via slots.json
1538
+ # rather than being reported as if the slot id were a feature name.
1539
+ from ..workspace.discovery import summarize_worktree_dirs
1540
+ active_worktrees: dict[str, list[str]] = summarize_worktree_dirs(root)
1545
1541
 
1546
1542
  return {
1547
1543
  "root": str(root),
@@ -1790,6 +1786,22 @@ def main():
1790
1786
  from .. import __version__
1791
1787
  print(f"canopy-mcp {__version__}")
1792
1788
  return
1789
+ if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
1790
+ print(
1791
+ "canopy-mcp — Canopy MCP server (stdio JSON-RPC)\n"
1792
+ "\n"
1793
+ "This is a Model Context Protocol server. It is not run interactively;\n"
1794
+ "your MCP-aware client (Claude Code, Claude Desktop, etc.) launches it\n"
1795
+ "and communicates with it over stdio.\n"
1796
+ "\n"
1797
+ "To register canopy with Claude Code, run:\n"
1798
+ " canopy setup-agent\n"
1799
+ "\n"
1800
+ "Options:\n"
1801
+ " -V, --version Print version and exit\n"
1802
+ " -h, --help Print this message and exit"
1803
+ )
1804
+ return
1793
1805
  mcp.run(transport="stdio")
1794
1806
 
1795
1807