canopy-cli 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. canopy_cli-3.1.0/.github/workflows/ci.yml +76 -0
  2. canopy_cli-3.1.0/.github/workflows/publish.yml +36 -0
  3. canopy_cli-3.1.0/.gitignore +17 -0
  4. canopy_cli-3.1.0/AGENTS.md +228 -0
  5. canopy_cli-3.1.0/CHANGELOG.md +115 -0
  6. canopy_cli-3.1.0/CLAUDE.md +162 -0
  7. canopy_cli-3.1.0/PKG-INFO +282 -0
  8. canopy_cli-3.1.0/README.md +254 -0
  9. canopy_cli-3.1.0/docs/agents.md +249 -0
  10. canopy_cli-3.1.0/docs/architecture/providers.md +372 -0
  11. canopy_cli-3.1.0/docs/architecture.md +321 -0
  12. canopy_cli-3.1.0/docs/archive/test-plan.md +326 -0
  13. canopy_cli-3.1.0/docs/canopy-banner.svg +95 -0
  14. canopy_cli-3.1.0/docs/cli-commit.svg +97 -0
  15. canopy_cli-3.1.0/docs/cli-config.svg +72 -0
  16. canopy_cli-3.1.0/docs/cli-done.svg +115 -0
  17. canopy_cli-3.1.0/docs/cli-drift.svg +103 -0
  18. canopy_cli-3.1.0/docs/cli-init.svg +128 -0
  19. canopy_cli-3.1.0/docs/cli-list.svg +114 -0
  20. canopy_cli-3.1.0/docs/cli-push.svg +90 -0
  21. canopy_cli-3.1.0/docs/cli-review.svg +136 -0
  22. canopy_cli-3.1.0/docs/cli-setup-agent.svg +75 -0
  23. canopy_cli-3.1.0/docs/cli-stage.svg +82 -0
  24. canopy_cli-3.1.0/docs/cli-state.svg +130 -0
  25. canopy_cli-3.1.0/docs/cli-status.svg +129 -0
  26. canopy_cli-3.1.0/docs/cli-switch.svg +117 -0
  27. canopy_cli-3.1.0/docs/cli-triage.svg +124 -0
  28. canopy_cli-3.1.0/docs/cli-worktree.svg +142 -0
  29. canopy_cli-3.1.0/docs/commands.md +211 -0
  30. canopy_cli-3.1.0/docs/concepts.md +277 -0
  31. canopy_cli-3.1.0/docs/mcp.md +217 -0
  32. canopy_cli-3.1.0/docs/workspace.md +329 -0
  33. canopy_cli-3.1.0/gen_svgs.py +284 -0
  34. canopy_cli-3.1.0/mockups/canopy-vscode.html +842 -0
  35. canopy_cli-3.1.0/pyproject.toml +55 -0
  36. canopy_cli-3.1.0/src/canopy/__init__.py +2 -0
  37. canopy_cli-3.1.0/src/canopy/actions/__init__.py +32 -0
  38. canopy_cli-3.1.0/src/canopy/actions/aliases.py +421 -0
  39. canopy_cli-3.1.0/src/canopy/actions/augments.py +55 -0
  40. canopy_cli-3.1.0/src/canopy/actions/bootstrap.py +249 -0
  41. canopy_cli-3.1.0/src/canopy/actions/bot_resolutions.py +123 -0
  42. canopy_cli-3.1.0/src/canopy/actions/bot_status.py +133 -0
  43. canopy_cli-3.1.0/src/canopy/actions/commit.py +511 -0
  44. canopy_cli-3.1.0/src/canopy/actions/conflicts.py +314 -0
  45. canopy_cli-3.1.0/src/canopy/actions/doctor.py +1459 -0
  46. canopy_cli-3.1.0/src/canopy/actions/draft_replies.py +185 -0
  47. canopy_cli-3.1.0/src/canopy/actions/drift.py +241 -0
  48. canopy_cli-3.1.0/src/canopy/actions/errors.py +115 -0
  49. canopy_cli-3.1.0/src/canopy/actions/evacuate.py +192 -0
  50. canopy_cli-3.1.0/src/canopy/actions/feature_state.py +607 -0
  51. canopy_cli-3.1.0/src/canopy/actions/historian.py +612 -0
  52. canopy_cli-3.1.0/src/canopy/actions/ide_workspace.py +49 -0
  53. canopy_cli-3.1.0/src/canopy/actions/last_visit.py +83 -0
  54. canopy_cli-3.1.0/src/canopy/actions/migrate_slots.py +313 -0
  55. canopy_cli-3.1.0/src/canopy/actions/preflight_state.py +97 -0
  56. canopy_cli-3.1.0/src/canopy/actions/push.py +199 -0
  57. canopy_cli-3.1.0/src/canopy/actions/reads.py +304 -0
  58. canopy_cli-3.1.0/src/canopy/actions/resume.py +582 -0
  59. canopy_cli-3.1.0/src/canopy/actions/review_filter.py +135 -0
  60. canopy_cli-3.1.0/src/canopy/actions/ship.py +399 -0
  61. canopy_cli-3.1.0/src/canopy/actions/slot_details.py +208 -0
  62. canopy_cli-3.1.0/src/canopy/actions/slot_load.py +383 -0
  63. canopy_cli-3.1.0/src/canopy/actions/slots.py +221 -0
  64. canopy_cli-3.1.0/src/canopy/actions/stash.py +230 -0
  65. canopy_cli-3.1.0/src/canopy/actions/switch.py +775 -0
  66. canopy_cli-3.1.0/src/canopy/actions/switch_preflight.py +192 -0
  67. canopy_cli-3.1.0/src/canopy/actions/thread_actions.py +88 -0
  68. canopy_cli-3.1.0/src/canopy/actions/thread_resolutions.py +101 -0
  69. canopy_cli-3.1.0/src/canopy/actions/triage.py +286 -0
  70. canopy_cli-3.1.0/src/canopy/agent/__init__.py +5 -0
  71. canopy_cli-3.1.0/src/canopy/agent/runner.py +129 -0
  72. canopy_cli-3.1.0/src/canopy/agent_setup/__init__.py +264 -0
  73. canopy_cli-3.1.0/src/canopy/agent_setup/skills/augment-canopy/SKILL.md +116 -0
  74. canopy_cli-3.1.0/src/canopy/agent_setup/skills/using-canopy/SKILL.md +191 -0
  75. canopy_cli-3.1.0/src/canopy/cli/__init__.py +0 -0
  76. canopy_cli-3.1.0/src/canopy/cli/main.py +4152 -0
  77. canopy_cli-3.1.0/src/canopy/cli/render.py +98 -0
  78. canopy_cli-3.1.0/src/canopy/cli/ui.py +150 -0
  79. canopy_cli-3.1.0/src/canopy/features/__init__.py +2 -0
  80. canopy_cli-3.1.0/src/canopy/features/coordinator.py +1256 -0
  81. canopy_cli-3.1.0/src/canopy/git/__init__.py +0 -0
  82. canopy_cli-3.1.0/src/canopy/git/hooks.py +173 -0
  83. canopy_cli-3.1.0/src/canopy/git/multi.py +435 -0
  84. canopy_cli-3.1.0/src/canopy/git/repo.py +859 -0
  85. canopy_cli-3.1.0/src/canopy/git/templates/post-checkout.py +67 -0
  86. canopy_cli-3.1.0/src/canopy/graph/__init__.py +0 -0
  87. canopy_cli-3.1.0/src/canopy/integrations/__init__.py +0 -0
  88. canopy_cli-3.1.0/src/canopy/integrations/github.py +983 -0
  89. canopy_cli-3.1.0/src/canopy/integrations/linear.py +307 -0
  90. canopy_cli-3.1.0/src/canopy/integrations/precommit.py +239 -0
  91. canopy_cli-3.1.0/src/canopy/mcp/__init__.py +0 -0
  92. canopy_cli-3.1.0/src/canopy/mcp/client.py +329 -0
  93. canopy_cli-3.1.0/src/canopy/mcp/server.py +1797 -0
  94. canopy_cli-3.1.0/src/canopy/providers/__init__.py +105 -0
  95. canopy_cli-3.1.0/src/canopy/providers/github_issues.py +289 -0
  96. canopy_cli-3.1.0/src/canopy/providers/linear.py +341 -0
  97. canopy_cli-3.1.0/src/canopy/providers/types.py +149 -0
  98. canopy_cli-3.1.0/src/canopy/workspace/__init__.py +4 -0
  99. canopy_cli-3.1.0/src/canopy/workspace/config.py +378 -0
  100. canopy_cli-3.1.0/src/canopy/workspace/context.py +224 -0
  101. canopy_cli-3.1.0/src/canopy/workspace/discovery.py +197 -0
  102. canopy_cli-3.1.0/src/canopy/workspace/workspace.py +173 -0
  103. canopy_cli-3.1.0/tests/__init__.py +0 -0
  104. canopy_cli-3.1.0/tests/conftest.py +221 -0
  105. canopy_cli-3.1.0/tests/test_action_errors.py +215 -0
  106. canopy_cli-3.1.0/tests/test_agent_setup.py +206 -0
  107. canopy_cli-3.1.0/tests/test_aliases.py +311 -0
  108. canopy_cli-3.1.0/tests/test_augments.py +108 -0
  109. canopy_cli-3.1.0/tests/test_bootstrap.py +210 -0
  110. canopy_cli-3.1.0/tests/test_bot_resolutions.py +133 -0
  111. canopy_cli-3.1.0/tests/test_bot_status.py +159 -0
  112. canopy_cli-3.1.0/tests/test_ci_status.py +148 -0
  113. canopy_cli-3.1.0/tests/test_commit.py +657 -0
  114. canopy_cli-3.1.0/tests/test_commit_push_integration.py +105 -0
  115. canopy_cli-3.1.0/tests/test_config.py +274 -0
  116. canopy_cli-3.1.0/tests/test_config_done.py +287 -0
  117. canopy_cli-3.1.0/tests/test_conflicts.py +155 -0
  118. canopy_cli-3.1.0/tests/test_context_stage.py +319 -0
  119. canopy_cli-3.1.0/tests/test_coordinator.py +381 -0
  120. canopy_cli-3.1.0/tests/test_doctor.py +896 -0
  121. canopy_cli-3.1.0/tests/test_draft_replies.py +162 -0
  122. canopy_cli-3.1.0/tests/test_drift.py +266 -0
  123. canopy_cli-3.1.0/tests/test_evacuate.py +179 -0
  124. canopy_cli-3.1.0/tests/test_feature_state.py +391 -0
  125. canopy_cli-3.1.0/tests/test_feature_state_bot.py +209 -0
  126. canopy_cli-3.1.0/tests/test_historian.py +322 -0
  127. canopy_cli-3.1.0/tests/test_hooks.py +227 -0
  128. canopy_cli-3.1.0/tests/test_last_visit.py +68 -0
  129. canopy_cli-3.1.0/tests/test_linear_integration.py +558 -0
  130. canopy_cli-3.1.0/tests/test_mcp_server_worktree_create.py +189 -0
  131. canopy_cli-3.1.0/tests/test_migrate_slots.py +128 -0
  132. canopy_cli-3.1.0/tests/test_new_commands.py +396 -0
  133. canopy_cli-3.1.0/tests/test_precommit.py +97 -0
  134. canopy_cli-3.1.0/tests/test_providers_github_issues.py +274 -0
  135. canopy_cli-3.1.0/tests/test_providers_linear.py +281 -0
  136. canopy_cli-3.1.0/tests/test_providers_registry.py +123 -0
  137. canopy_cli-3.1.0/tests/test_providers_types.py +78 -0
  138. canopy_cli-3.1.0/tests/test_push.py +230 -0
  139. canopy_cli-3.1.0/tests/test_reads.py +284 -0
  140. canopy_cli-3.1.0/tests/test_repo.py +329 -0
  141. canopy_cli-3.1.0/tests/test_resume.py +1694 -0
  142. canopy_cli-3.1.0/tests/test_review.py +417 -0
  143. canopy_cli-3.1.0/tests/test_review_filter.py +193 -0
  144. canopy_cli-3.1.0/tests/test_runner.py +143 -0
  145. canopy_cli-3.1.0/tests/test_ship.py +130 -0
  146. canopy_cli-3.1.0/tests/test_slot_details.py +45 -0
  147. canopy_cli-3.1.0/tests/test_slot_load.py +139 -0
  148. canopy_cli-3.1.0/tests/test_slots.py +172 -0
  149. canopy_cli-3.1.0/tests/test_stash_features.py +250 -0
  150. canopy_cli-3.1.0/tests/test_switch.py +333 -0
  151. canopy_cli-3.1.0/tests/test_switch_action.py +347 -0
  152. canopy_cli-3.1.0/tests/test_switch_preflight.py +47 -0
  153. canopy_cli-3.1.0/tests/test_thread_actions.py +271 -0
  154. canopy_cli-3.1.0/tests/test_thread_graphql.py +160 -0
  155. canopy_cli-3.1.0/tests/test_triage.py +377 -0
  156. canopy_cli-3.1.0/tests/test_workspace.py +113 -0
  157. canopy_cli-3.1.0/tests/test_worktree_features.py +318 -0
  158. canopy_cli-3.1.0/vscode-extension/.gitignore +5 -0
  159. canopy_cli-3.1.0/vscode-extension/.vscodeignore +14 -0
  160. canopy_cli-3.1.0/vscode-extension/CHANGELOG.md +128 -0
  161. canopy_cli-3.1.0/vscode-extension/LICENSE +21 -0
  162. canopy_cli-3.1.0/vscode-extension/README.md +82 -0
  163. canopy_cli-3.1.0/vscode-extension/esbuild.config.mjs +71 -0
  164. canopy_cli-3.1.0/vscode-extension/jest.config.ts +22 -0
  165. canopy_cli-3.1.0/vscode-extension/media/canopy-icon.png +0 -0
  166. canopy_cli-3.1.0/vscode-extension/media/canopy-icon.svg +588 -0
  167. canopy_cli-3.1.0/vscode-extension/media/screenshots/dashboard-feature.png +0 -0
  168. canopy_cli-3.1.0/vscode-extension/media/screenshots/dashboard-global.png +0 -0
  169. canopy_cli-3.1.0/vscode-extension/package-lock.json +9054 -0
  170. canopy_cli-3.1.0/vscode-extension/package.json +297 -0
  171. canopy_cli-3.1.0/vscode-extension/src/__mocks__/vscode.ts +98 -0
  172. canopy_cli-3.1.0/vscode-extension/src/canopyCli.test.ts +196 -0
  173. canopy_cli-3.1.0/vscode-extension/src/canopyCli.ts +862 -0
  174. canopy_cli-3.1.0/vscode-extension/src/canopyClient.ts +704 -0
  175. canopy_cli-3.1.0/vscode-extension/src/cliResolver.test.ts +122 -0
  176. canopy_cli-3.1.0/vscode-extension/src/cliResolver.ts +134 -0
  177. canopy_cli-3.1.0/vscode-extension/src/commands/createFeature.ts +161 -0
  178. canopy_cli-3.1.0/vscode-extension/src/commands/createFeatureFromIssue.ts +85 -0
  179. canopy_cli-3.1.0/vscode-extension/src/commands/installBackend.ts +218 -0
  180. canopy_cli-3.1.0/vscode-extension/src/commands/setupWizard.ts +103 -0
  181. canopy_cli-3.1.0/vscode-extension/src/extension.ts +828 -0
  182. canopy_cli-3.1.0/vscode-extension/src/mcpResolver.ts +130 -0
  183. canopy_cli-3.1.0/vscode-extension/src/stateReader.test.ts +273 -0
  184. canopy_cli-3.1.0/vscode-extension/src/stateReader.ts +287 -0
  185. canopy_cli-3.1.0/vscode-extension/src/statusBar.ts +96 -0
  186. canopy_cli-3.1.0/vscode-extension/src/types.ts +192 -0
  187. canopy_cli-3.1.0/vscode-extension/src/views/GlobalDashboardPanel.ts +1020 -0
  188. canopy_cli-3.1.0/vscode-extension/src/views/canopyTreeProvider.ts +269 -0
  189. canopy_cli-3.1.0/vscode-extension/src/views/themeShim.ts +113 -0
  190. canopy_cli-3.1.0/vscode-extension/src/watchers.ts +79 -0
  191. canopy_cli-3.1.0/vscode-extension/src/webview/cockpitPanel.ts +395 -0
  192. canopy_cli-3.1.0/vscode-extension/src/webview/components/branchLedger.ts +72 -0
  193. canopy_cli-3.1.0/vscode-extension/src/webview/components/bridge.ts +72 -0
  194. canopy_cli-3.1.0/vscode-extension/src/webview/components/capReachedModal.ts +112 -0
  195. canopy_cli-3.1.0/vscode-extension/src/webview/components/focusTile.ts +139 -0
  196. canopy_cli-3.1.0/vscode-extension/src/webview/components/newFeatureForm.ts +174 -0
  197. canopy_cli-3.1.0/vscode-extension/src/webview/components/styles.ts +691 -0
  198. canopy_cli-3.1.0/vscode-extension/src/webview/components/triageFeed.ts +91 -0
  199. canopy_cli-3.1.0/vscode-extension/src/webview/components/util.ts +40 -0
  200. canopy_cli-3.1.0/vscode-extension/src/webview/components/worktreeRow.ts +71 -0
  201. canopy_cli-3.1.0/vscode-extension/src/webview/dashboardPanel.ts +904 -0
  202. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/Dashboard.tsx +137 -0
  203. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/FeatureView.tsx +972 -0
  204. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/GlobalView.tsx +649 -0
  205. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/Skeletons.tsx +38 -0
  206. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/diff.ts +118 -0
  207. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/index.tsx +17 -0
  208. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/protocol.ts +129 -0
  209. canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/vscode.ts +30 -0
  210. canopy_cli-3.1.0/vscode-extension/src/webview/newFeaturePanel.ts +322 -0
  211. canopy_cli-3.1.0/vscode-extension/src/webview/shared/pastel.css +801 -0
  212. canopy_cli-3.1.0/vscode-extension/src/webview/themes/index.ts +42 -0
  213. canopy_cli-3.1.0/vscode-extension/src/webview/themes/minimal.ts +62 -0
  214. canopy_cli-3.1.0/vscode-extension/src/webview/themes/navy.ts +60 -0
  215. canopy_cli-3.1.0/vscode-extension/src/webview/themes/render.ts +96 -0
  216. canopy_cli-3.1.0/vscode-extension/src/webview/themes/types.ts +104 -0
  217. canopy_cli-3.1.0/vscode-extension/tsconfig.json +20 -0
@@ -0,0 +1,76 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ python-tests:
14
+ name: pytest (Python ${{ matrix.python }})
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python: ["3.10", "3.11", "3.12"]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python }}
26
+ cache: pip
27
+
28
+ # Tests create real Git repos — give them a deterministic identity.
29
+ - name: Configure Git
30
+ run: |
31
+ git config --global user.email "ci@canopy.dev"
32
+ git config --global user.name "Canopy CI"
33
+ git config --global init.defaultBranch main
34
+
35
+ - name: Install package (with dev extras)
36
+ run: |
37
+ python -m pip install --upgrade pip
38
+ pip install -e ".[dev]"
39
+
40
+ - name: Run pytest
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,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ name: Build and publish to PyPI
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ id-token: write # OIDC for PyPI trusted publishing
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Build sdist and wheel
22
+ run: |
23
+ python -m pip install --upgrade pip build
24
+ python -m build
25
+
26
+ - name: Verify version matches tag
27
+ run: |
28
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
29
+ PKG_VERSION=$(python -c "import re,pathlib; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', pathlib.Path('src/canopy/__init__.py').read_text()).group(1))")
30
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
31
+ echo "::error::Tag v$TAG_VERSION does not match __version__ ($PKG_VERSION)"
32
+ exit 1
33
+ fi
34
+
35
+ - name: Publish to PyPI
36
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ pytest-cache-files-*/
9
+ *.egg
10
+ .venv/
11
+ venv/
12
+ .env
13
+ .canopy/
14
+ .worktrees/
15
+ node_modules/
16
+ .DS_Store
17
+ random/
@@ -0,0 +1,228 @@
1
+ # Canopy — Contributor Guide for AI Agents
2
+
3
+ This file is the "how to extend canopy without breaking module boundaries" guide.
4
+ It is CLAUDE.md's sibling, not its replacement:
5
+
6
+ - `CLAUDE.md` — what canopy is, architecture overview, key conventions, MCP tool list
7
+ - `docs/concepts.md` — the four conceptual pillars (action framework, context contract, state machine, slot model, resume brief)
8
+ - `docs/architecture.md` — formal module reference
9
+
10
+ ## Before You Start
11
+
12
+ 1. Read `CLAUDE.md`. It has the architecture diagram, slot model explanation, and all key conventions.
13
+ 2. Read `docs/concepts.md` if you need the vocabulary for the state machine or action framework.
14
+ 3. Run the test suite to confirm your baseline: `pytest tests/ -v` (857 tests, ~225s).
15
+ 4. If you are adding to the slot model or switch flow, read `actions/slots.py` and `actions/switch.py` first.
16
+
17
+ ## Module Boundaries
18
+
19
+ These are hard rules. Do not break them.
20
+
21
+ - **`git/repo.py` is the only file that calls `subprocess.run(["git", ...])`.**
22
+ New git operations belong here. Everything else calls functions from this module.
23
+
24
+ - **`git/multi.py` handles cross-repo git operations.** It calls `git/repo.py` functions;
25
+ it does not shell out to git directly.
26
+
27
+ - **`mcp/server.py` and `cli/main.py` are thin wrappers.**
28
+ Business logic lives in `actions/`, `features/coordinator.py`, `git/multi.py`,
29
+ and `workspace/`. Neither the MCP server nor the CLI should own logic.
30
+
31
+ - **All actions live in `actions/`.** This is the most-modified directory. Each action
32
+ module owns one concern: `switch.py` owns slot focus, `slots.py` owns slot state
33
+ reads/writes, `drift.py` owns drift detection, `resume.py` owns the resume brief, etc.
34
+
35
+ - **Actions raise `BlockerError` for precondition failures.**
36
+ Shape: `BlockerError(code, what, expected, actual, fix_actions, details)`.
37
+ CLI renders via `cli/render.py`. MCP returns `to_dict()`. Same shape, two consumers.
38
+
39
+ - **Universal aliases — every read tool accepts multiple forms.**
40
+ Feature name, Linear ID, `<repo>#<n>`, PR URL, `<repo>:<branch>`, or slot id
41
+ (`worktree-N` → slot's current occupant). Always resolve via
42
+ `actions/aliases.py:resolve_feature`. Never reimplement alias resolution inline.
43
+
44
+ - **Per-repo branches map — never assume branch == feature name.**
45
+ Use `lane.branch_for(repo)` or `repos_for_feature(workspace, feature)`.
46
+ `FeatureLane.repos` alone does not give you branch names for legacy features.
47
+
48
+ - **All integrations go through `mcp/client.py` or the `gh` CLI fallback.**
49
+ Integration modules in `integrations/` never call external APIs directly.
50
+ `integrations/github.py` falls back to `gh api` / `gh pr` when no MCP server
51
+ is configured. If neither is available, raise `BlockerError(code='github_not_configured')`.
52
+
53
+ - **`integrations/precommit.py` is the one exception to the MCP-only rule.**
54
+ It runs local hooks via subprocess. This is intentional — hooks run locally.
55
+
56
+ ## State Files
57
+
58
+ All state is under `.canopy/state/`. OAuth tokens at `~/.canopy/mcp-tokens/`.
59
+
60
+ | File | Owner | Notes |
61
+ |---|---|---|
62
+ | `heads.json` | `git/hooks.py` + post-checkout hook | Written by hook; read by `drift.py`, `historian.py` |
63
+ | `slots.json` | `actions/slots.py` | Canonical + warm slot occupancy, `last_touched` LRU, `in_flight` marker |
64
+ | `preflight.json` | `actions/preflight_state.py` | Records preflight result per feature |
65
+ | `visits.json` | `actions/last_visit.py` | Per-feature `last_visit` / `previous_visit` ISO timestamps |
66
+ | `thread_resolutions.json` | `actions/thread_resolutions.py` | Resolved GitHub review threads |
67
+ | `bot_resolutions.json` | `actions/bot_resolutions.py` | Bot-comment resolutions addressed via `commit --address` |
68
+
69
+ All state writes use atomic temp+rename (`tmp.replace(path)`) to prevent corruption
70
+ from concurrent agents. See `actions/slots.py` for the canonical pattern.
71
+
72
+ ## Adding a New Action
73
+
74
+ This is the most common change.
75
+
76
+ 1. Create `src/canopy/actions/<name>.py`. Raise `BlockerError` for preconditions.
77
+ Use existing fixtures and patterns; don't re-invent error shapes.
78
+
79
+ 2. Expose via CLI in `cli/main.py`:
80
+ - Add `cmd_<name>(args: argparse.Namespace) -> None`
81
+ - Add a subparser in `main()`
82
+ - Dispatch via the `commands` dict
83
+ - Support `--json` via `_print_json()`
84
+
85
+ 3. Expose via MCP in `mcp/server.py`:
86
+ - Add `@mcp.tool()` wrapper that calls the action function
87
+ - Write a clear docstring — it becomes the tool description agents see
88
+
89
+ 4. Add tests in `tests/test_<name>.py` using the `workspace_with_feature` fixture
90
+ (or another fixture from `tests/conftest.py`).
91
+
92
+ 5. If the action is user-facing:
93
+ - Update `docs/commands.md`
94
+ - Update `docs/mcp.md`
95
+ - Update the architecture box and MCP-tool-group listing in `CLAUDE.md`
96
+
97
+ 6. If agents need to know when to prefer the new tool, update
98
+ `~/.claude/skills/using-canopy/SKILL.md` and
99
+ `src/canopy/agent_setup/skills/using-canopy/SKILL.md`.
100
+
101
+ ## Adding a New CLI Command Only
102
+
103
+ When a new subcommand wraps existing actions without needing a new action module:
104
+
105
+ 1. Add `cmd_<name>(args)` in `cli/main.py` calling existing action functions.
106
+ 2. Add subparser in `main()`, dispatch in the `commands` dict.
107
+ 3. Support `--json` via `_print_json()`.
108
+ 4. Human-readable output: 2-space indent, `─` for separators.
109
+ 5. Update `docs/commands.md`.
110
+
111
+ ## Adding a New MCP Tool Only
112
+
113
+ When the action already exists and you just need to expose it:
114
+
115
+ 1. Add `@mcp.tool()` in `mcp/server.py`. Call the existing action function directly.
116
+ 2. Return dicts/lists (FastMCP handles JSON serialization).
117
+ 3. Write a docstring — it is the tool description.
118
+ 4. Update `docs/mcp.md` and the tool-group listing in `CLAUDE.md`.
119
+
120
+ ## Adding a New Git Operation
121
+
122
+ 1. Add the function to `git/repo.py` using `_run()` or `_run_ok()`.
123
+ Prefer `_run()` (raises on failure) for write operations.
124
+ `_run_ok()` (returns empty string on failure) is only safe for reads.
125
+ 2. Write a test in `tests/test_repo.py`.
126
+ 3. Do not call `subprocess.run(["git", ...])` anywhere else.
127
+
128
+ ## Adding a New State File
129
+
130
+ 1. Create `actions/<name>.py` with a module docstring naming the path
131
+ (e.g., `State file: .canopy/state/<name>.json`).
132
+ 2. Use the atomic temp+rename pattern from `actions/slots.py` or
133
+ `actions/thread_resolutions.py`:
134
+ ```python
135
+ tmp = path.with_suffix(".tmp")
136
+ tmp.write_text(json.dumps(data))
137
+ tmp.replace(path)
138
+ ```
139
+ 3. Update the state-files table in this file.
140
+ 4. Update the state-files line in `CLAUDE.md`.
141
+ 5. Update the state-files table in `docs/architecture.md` and
142
+ the state-files section in `docs/workspace.md`.
143
+
144
+ ## Adding a New Integration
145
+
146
+ 1. Add a module in `integrations/`.
147
+ 2. Use `mcp/client.py` to call the MCP server, or `gh` CLI as fallback.
148
+ 3. Check for server presence via `mcp.client.get_mcp_config()` before calling.
149
+ 4. Handle `McpClientError` gracefully — never fail the whole operation because
150
+ an integration is unavailable.
151
+ 5. If the integration is Linear or GitHub, link metadata into `features.json`
152
+ via `FeatureLane` fields rather than a separate sidecar file.
153
+ 6. Write tests that mock the MCP call but test the data flow end-to-end.
154
+
155
+ ## Adding a New Bundled Skill
156
+
157
+ 1. Create `src/canopy/agent_setup/skills/<name>/SKILL.md`.
158
+ 2. Default skills (always installed on `canopy setup-agent`) must be declared
159
+ in `agent_setup/__init__.py`.
160
+ 3. Opt-in skills install via `canopy setup-agent --skill <name>`.
161
+ 4. Document the new skill in `docs/agents.md` under the skills section.
162
+ 5. Foreign skills at the same install path are not overwritten without `--reinstall`.
163
+
164
+ ## Adding a New Augment Key
165
+
166
+ 1. Update `src/canopy/actions/augments.py`.
167
+ The resolver is intentionally lenient — unknown keys are silently preserved,
168
+ so adding a new key is backward-compatible.
169
+ 2. Consume the new key in whichever action or integration needs it via
170
+ `repo_augments(workspace, repo_name).get("<key>")`.
171
+ 3. Document the key in the recognized-keys table in
172
+ `src/canopy/agent_setup/skills/augment-canopy/SKILL.md`.
173
+ 4. Add a `canopy doctor` check if misconfiguration has a clear error form.
174
+
175
+ ## Testing Conventions
176
+
177
+ - All tests use real temporary Git repos, not mocks. This catches real git behavior.
178
+ - Fixtures are in `tests/conftest.py`. Key fixtures:
179
+ - `workspace_dir` — bare workspace with `api/` and `ui/` repos on main
180
+ - `workspace_with_feature` — workspace with `auth-flow` branches + commits in both repos
181
+ - `canopy_toml` — workspace with a canopy.toml already written
182
+ - Test file naming: `test_<module>.py` or `test_<feature_area>.py`.
183
+ - Worktree tests must clean up with `git worktree remove` when done.
184
+ - Run: `pytest tests/ -v` from the `canopy/` directory.
185
+
186
+ ## JSON Output Contract
187
+
188
+ Every `--json` command and MCP tool returns structured data. The shape is the contract
189
+ and is defined in each action's docstring. CLI and MCP return identical bytes.
190
+
191
+ Key shapes:
192
+
193
+ | Tool / command | Root shape |
194
+ |---|---|
195
+ | `workspace_status` | `WorkspaceStatus` (see `Workspace.to_dict()`) |
196
+ | `feature_list` | `list[FeatureLane.to_dict()]` |
197
+ | `feature_status` | `FeatureLane.to_dict()` with `repo_states` |
198
+ | `feature_state` | 9-state machine result + `summary` + `next_actions` |
199
+ | `feature_resume` | `version: 1`, `since_last_visit`, `current_state`, `intent_hints` |
200
+ | `slots(rich=True)` | `canonical` + per-slot enriched dashboard payload |
201
+ | `triage` | priority-tiered cross-repo PR enumeration |
202
+
203
+ ## Context Detection
204
+
205
+ `workspace/context.py` distinguishes four context types based on cwd:
206
+
207
+ 1. `feature_dir` — inside `.canopy/worktrees/worktree-N/` (slot root; all repos in scope)
208
+ 2. `repo_worktree` — inside `.canopy/worktrees/worktree-N/<repo>/` (single repo)
209
+ 3. `repo` — inside a normal workspace repo (feature = current branch if non-default)
210
+ 4. `workspace_root` — at the canopy.toml level (all repos in scope)
211
+
212
+ Used by `canopy stage` and other context-sensitive commands.
213
+
214
+ ## Version Handshake
215
+
216
+ When shipping a milestone:
217
+
218
+ 1. Bump `__version__` in `src/canopy/__init__.py`.
219
+ 2. Add a section to `CHANGELOG.md`.
220
+ 3. Doctor's `cli_stale` / `mcp_stale` checks compare against this version —
221
+ the handshake is only useful if the number actually moves.
222
+
223
+ ## Hooks Safety
224
+
225
+ `git/templates/post-checkout.py` uses `fcntl.flock` and atomic rename so concurrent
226
+ fires across repos in the same workspace don't race. It chains any pre-existing user
227
+ hooks and is installed by `canopy init` (or `canopy hooks install`). Worktrees inherit
228
+ hooks via the git `commondir` mechanism.
@@ -0,0 +1,115 @@
1
+ # Canopy CLI / MCP — Changelog
2
+
3
+ Tracks the Python side (CLI + MCP server). The VSCode extension has its own [vscode-extension/CHANGELOG.md](vscode-extension/CHANGELOG.md).
4
+
5
+ Versions follow semver. Pre-1.0 — minor bumps may add features or break behavior; the README is the source-of-truth contract.
6
+
7
+ ## 3.1.0 — 2026-05-30 (Plan 2 — Feature Resume)
8
+
9
+ ### Added
10
+ - `canopy resume <alias>` (+ `mcp__canopy__feature_resume`): switch-aware
11
+ compound action. One call: alias → switch-if-needed → refresh GitHub + Linear →
12
+ compute structured brief with `intent_hints` for the most likely next actions.
13
+ See `docs/concepts.md#returning-to-a-feature`.
14
+ - `canopy resolve <thread_id>` (+ `mcp__canopy__resolve_thread`): close a
15
+ GitHub review thread + log to `.canopy/state/thread_resolutions.json` for
16
+ attribution in the resume brief.
17
+ - `canopy reply <thread_id> [--body | --body-file | stdin]`
18
+ (+ `mcp__canopy__reply_to_thread`): post a reply to a GH review thread.
19
+ `--resolve` (or `resolve_after=True`) closes the thread after posting.
20
+ - `canopy commit --address <id> --resolve-thread`: optionally close the GH
21
+ review thread after the local commit. Augment
22
+ `auto_resolve_threads_on_address = true` in canopy.toml makes this the
23
+ default for the workspace. `--no-resolve-thread` overrides the augment
24
+ per-invocation.
25
+ - New state files: `.canopy/state/visits.json` (per-feature last-visit anchor
26
+ `{feature: {last_visit, previous_visit}}`); `.canopy/state/thread_resolutions.json`
27
+ (canopy-driven GH thread closures `{thread_id: {resolved_by_canopy_at,
28
+ feature, via_command, via_commit_sha}}`).
29
+ - `actions/last_visit.py` — get/mark/reset the per-feature visit anchor.
30
+ - `actions/resume.py` — `feature_resume` compound action + `resume_summary`
31
+ (counts-only view embedded in `switch` return).
32
+ - `actions/thread_actions.py` — `resolve_thread` + `reply_to_thread` wrappers
33
+ + local resolution log writer.
34
+ - `actions/thread_resolutions.py` — load/record/filter_since for the
35
+ thread-resolutions log.
36
+ - GraphQL thread API in `integrations/github.py`: `list_review_threads`,
37
+ `resolve_thread`, `unresolve_thread`, `reply_to_thread`. Every comment from
38
+ `get_review_comments` now carries a `thread_id` field (GraphQL-sourced when
39
+ available, `""` on REST fallback) and `author_type` from GraphQL `__typename`.
40
+ - Bundled `using-canopy` skill now teaches `feature_resume` as the
41
+ session-start primitive and documents the "Closing out review threads"
42
+ workflow.
43
+
44
+ ### Changed
45
+ - `switch(feature)` bumps `last_visit` on every successful switch and embeds
46
+ `since_last_visit_summary` in its return value — a counts-only view
47
+ (commits, threads, GH resolutions, draft replies) so the agent sees
48
+ "something changed" without a full `feature_resume` round-trip. Sets
49
+ `degraded: true` if GitHub is unreachable.
50
+ - `get_review_comments` prefers GraphQL when available (single round-trip for
51
+ thread IDs + `author_type`); falls back to REST with `thread_id=""`.
52
+
53
+ ### Notes
54
+ - `feature_resume` refreshes GitHub + Linear on every call — the brief is
55
+ never cached at the canopy layer.
56
+ - Plan 1's slot model is the prerequisite. If upgrading from pre-3.0, run
57
+ `canopy migrate-slots` first.
58
+
59
+ ## 3.0.0 — 2026-05-28 (Wave 3.0)
60
+
61
+ **Breaking — slot model.** Worktree directories are now generic numbered slots (`worktree-1`, `worktree-2`, ...) instead of feature-named. `max_worktrees` renamed to `slots` (default 2). State unified in `.canopy/state/slots.json`; `active_feature.json` deleted. Run `canopy migrate-slots` once per workspace.
62
+
63
+ ### Added
64
+ - `canopy slots` / `mcp__canopy__slots` — inspect canonical + warm slot occupancy. `slots --rich` returns the full dashboard shape (branch, dirty, ahead/behind, PR, CI, bots, linear, last commit, feature_state per slot+canonical).
65
+ - `canopy slot load <feature> [<slot-N>]` — warm a cold feature into a slot without changing canonical. Optional `--replace` evicts the occupant first.
66
+ - `canopy slot clear <slot-N>` — evict a slot's occupant to cold with feature-tagged stash.
67
+ - `canopy slot swap <slot-A> <slot-B>` — exchange the occupants of two slots (identical-scope features only in v1).
68
+ - `canopy switch <feature> --evict-to <slot-N>` — pin the destination slot for the outgoing canonical.
69
+ - `canopy switch --to-slot <slot-N>` — promote whatever feature occupies that slot.
70
+ - `canopy migrate-slots` / `mcp__canopy__migrate_slots` — one-shot pre-3.0 → 3.0 migration (with dry-run preflight and rollback safety).
71
+ - Slot id (`worktree-N`) is a universal alias form — any tool that takes a feature alias also accepts it.
72
+ - Doctor categories: `slot_dir_orphan`, `slot_entry_orphan`, `slot_branch_mismatch`, `slot_detached_head` (info severity for the bisect/detached case).
73
+ - Fast-path swap: when Y is already warm, `switch(Y)` is 5 git ops per repo + 1 JSON write. Closes issue #3.
74
+ - Transaction safety: `slots.json.in_flight` marker recorded on partial multi-repo switch/swap failures so subsequent operations refuse to compound the damage.
75
+
76
+ ### Changed
77
+ - `canopy.toml`: `[workspace] max_worktrees = N` → `[workspace] slots = N`. Default 2 (was 0 = unlimited).
78
+ - Worktree layout: `.canopy/worktrees/<feature>/<repo>/` → `.canopy/worktrees/worktree-N/<repo>/`.
79
+ - MCP tools `worktree_create`, `worktree_info`, `workspace_status` return slot-keyed shapes. `slots` MCP tool defaults to `rich=True`.
80
+ - `slot_load` now requires the feature to be registered (`feature create` first) — no more silent "treat as all repos" fallback for unregistered names.
81
+
82
+ ### Removed
83
+ - `actions/active_feature.py` (folded into `actions/slots.py`).
84
+ - `actions/realign.py` (deprecated since 2.9).
85
+ - Pre-2.9 lazy migration path inside `switch` (replaced by explicit `canopy migrate-slots` + a `pre_migration` BlockerError that points at it).
86
+
87
+ ## 0.7.0
88
+
89
+ Five new top-level commands + a CI-aware state machine.
90
+
91
+ - **`canopy ship`** takes a feature from "code is committed" to "PR is open across every repo." Per-repo recipe: ensure-pushed → ensure-PR-exists → cross-repo body refresh so each PR description links to its siblings. Idempotent (`up_to_date` on re-run); refuses to silently recreate closed PRs; surfaces force-push divergence as `diverged`. Exposed as `mcp__canopy__ship`.
92
+ - **`canopy draft-replies <alias>`** walks each unresolved review comment's anchor sha through `git log -- <path>`. Addressed comments get a template-based draft (`Done — <subject>. (<sha>)` / `Addressed in <sha>: <subject>.` / `Addressed across N commits — <list>.`) with high/medium/low confidence based on commit count + keyword overlap. No LLM. Exposed as `mcp__canopy__draft_replies`.
93
+ - **CI status integration.** `feature_state.repos[*].pr.ci_status` carries a rolled-up CI verdict from `gh pr checks`. The state machine gains `awaiting_ci` (approved + pending CI), and approved + failing CI now flips to `needs_work` instead of misleadingly reporting `approved`. New `canopy pr-checks <alias>` + `mcp__canopy__pr_checks` for the raw check list.
94
+ - **`canopy worktree-bootstrap <feature>`** runs three optional steps per repo: copy `env_files` from main checkout into the worktree, run `install_cmd` (e.g. `uv sync` / `pnpm install`), and write `.canopy/workspaces/<feature>.code-workspace` when `[workspace] ide = "vscode"`. New per-repo `env_files` / `install_cmd` / `ide_settings` keys in canopy.toml; per-workspace `bootstrap_default` opt-in.
95
+ - **`canopy conflicts`** pairwise intersects each active feature's changed-files per repo. `--lines` opts into a deeper `git diff --unified=0` parse for line-range overlap (downgrades to `medium` when files overlap but lines don't); generated/lockfile-style files auto-drop to `medium` with an "auto-mergeable" suggestion. Exposed as `mcp__canopy__conflicts`.
96
+
97
+ MCP tools 54 → 59. Tests 651 → 712. Comment-shape adds `commit_id` so the file-history walk in draft-replies can anchor properly.
98
+
99
+ ## 0.5.0
100
+
101
+ Catches the `__version__` constant up to ~6 months of shipped work. The handshake the doctor's staleness checks rely on is only useful when this number actually moves — this release fixes that drift.
102
+
103
+ - **`canopy commit` + `canopy push`** — feature-scoped multi-repo commit and push with `wrong_branch` / `no_upstream` blockers and per-repo result classification.
104
+ - **Provider-injection architecture** — `docs/architecture/providers.md` design doc for the issue-provider contract.
105
+ - **`canopy doctor`** — single recovery primitive with 16 diagnostic categories (state-file integrity + install / version / mcp / skill / vsix). `--fix` for auto-repairable; severity tiers; structured JSON.
106
+ - **Issue providers** — `IssueProvider` Protocol + registry under `canopy.providers.*`. Linear refactored into the contract; `GitHubIssuesProvider` via `gh` CLI. `[issue_provider]` block in canopy.toml; `issue_get` / `issue_list_my_issues` MCP tools (deprecated `linear_*` aliases retained).
107
+ - **Augments** — per-workspace `[augments]` block in canopy.toml + per-repo overrides. `preflight_cmd` is the first consumer; `review_bots` and `test_cmd` reserved. Multi-skill installer; `augment-canopy` skill teaches the agent how to mutate canopy.toml safely.
108
+ - **Bot-comment tracking** — per-comment resolution log at `.canopy/state/bot_resolutions.json`; `canopy commit --address <comment-id>` auto-suffixes the message and records the resolution; `canopy bot-status` rollup; new `awaiting_bot_resolution` state.
109
+ - **Historian** — per-feature persistent memory at `.canopy/memory/<feature>.md`. Auto-read on `canopy switch` (response carries `memory: <markdown>`); 5 MCP tools (`historian_decide` / `historian_pause` / `historian_defer_comment` / `feature_memory` / `historian_compact`); 2 CLI commands (`canopy historian show` / `compact`); auto-mirror from `commit --address` and `github_get_pr_comments`.
110
+
111
+ MCP tools 41 → 54. Tests ~400 → 624. State machine 8 → 9 (added `awaiting_bot_resolution`). Bundled skills 1 → 2 (`using-canopy` + opt-in `augment-canopy`).
112
+
113
+ ## 0.1.0
114
+
115
+ Initial release: workspace discovery, feature lanes, post-checkout hook, drift detection, `switch` / `triage` / `feature_state` actions, MCP server, agent setup.
@@ -0,0 +1,162 @@
1
+ # Canopy — Claude Code Context
2
+
3
+ ## What This Project Is
4
+
5
+ Canopy is the **context contract** between an AI coding agent and a multi-repo workspace, plus a **drift-proof CLI** for the human. Every operation takes semantic context (`feature`, `repo`, alias) and resolves paths internally — the agent literally can't `cd` to the wrong directory because it never specifies a directory. Multi-repo drift is detected in real time via per-repo post-checkout hooks and surfaced as a structured `BlockerError`. PR review comments are temporally classified into `actionable_threads` vs `likely_resolved_threads`, so the agent's context budget goes to comprehension, not orchestration.
6
+
7
+ **`canopy switch` is the focus primitive (Wave 3.0 slot model).** Each feature lives in one of three states: **canonical** (checked out in main repo — the only place code is meant to run), **warm** (occupies a numbered slot at `.canopy/worktrees/worktree-N/<repo>/`), **cold** (branch only). Slots are stable disk resources; features are transient tenants — a slot keeps its id (`worktree-1`, `worktree-2`, ...) across feature swaps. `switch(Y)` promotes Y to canonical; previously-canonical X either evacuates into a warm slot (active rotation, default — instant to switch back) or goes cold with a feature-tagged stash (wind-down via `--release-current`). Cap (`slots`, default 2) protects against unbounded growth via LRU eviction or a `worktree_cap_reached` BlockerError. See [docs/concepts.md §4](docs/concepts.md#4-the-slot-model).
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ src/canopy/
13
+ ├── cli/main.py # argparse entry point, all commands (thin wrapper)
14
+ ├── cli/render.py # structured-error renderer
15
+ ├── workspace/
16
+ │ ├── config.py # canopy.toml parser
17
+ │ ├── discovery.py # auto-detect repos + worktrees, generate toml
18
+ │ ├── context.py # context detection (feature_dir, repo_worktree, repo, workspace_root)
19
+ │ └── workspace.py # Workspace class, RepoState dataclass
20
+ ├── git/
21
+ │ ├── repo.py # ALL git subprocess calls go here
22
+ │ ├── multi.py # cross-repo operations
23
+ │ ├── hooks.py # install/uninstall post-checkout hook + heads.json reader
24
+ │ └── templates/post-checkout.py # hook script (Python, fcntl-locked, never blocks git)
25
+ ├── features/coordinator.py # FeatureLane, FeatureCoordinator (+ branches map for per-repo branches)
26
+ ├── actions/ # WAVE 2+: action layer — completion-driven recipes
27
+ │ ├── errors.py # ActionError / BlockerError / FailedError / FixAction
28
+ │ ├── aliases.py # universal alias resolver (incl. worktree-N → slot occupant)
29
+ │ ├── slots.py # WAVE 3.0: slots.json reader/writer + path resolution + LRU
30
+ │ ├── slot_load.py # WAVE 3.0: slot_load / slot_clear / slot_swap primitives
31
+ │ ├── slot_details.py # WAVE 3.0: rich slots shape (PR/CI/bots/linear per slot+canonical)
32
+ │ ├── migrate_slots.py # WAVE 3.0: one-shot pre-3.0 → 3.0 layout migration
33
+ │ ├── drift.py # detect_drift + assert_aligned (cached path)
34
+ │ ├── evacuate.py # WAVE 2.9: per-repo evacuate primitive (stash → wt-add → pop)
35
+ │ ├── feature_state.py # 9-state machine, dashboard backend (live git, worktree-aware)
36
+ │ ├── bot_resolutions.py # M3: persistent log of bot comments addressed via `commit --address`
37
+ │ ├── bot_status.py # M3: per-feature bot-comment rollup
38
+ │ ├── augments.py # M2: per-workspace augment resolver (preflight_cmd, review_bots, ...)
39
+ │ ├── bootstrap.py # M6: env-file copy + install_cmd + IDE workspace gen for worktrees
40
+ │ ├── conflicts.py # M12: cross-feature file/line overlap detection
41
+ │ ├── draft_replies.py # M9: file-history-based addressed-comment classifier + reply templates
42
+ │ ├── historian.py # M4: cross-session feature memory at .canopy/memory/<feature>.md
43
+ │ ├── ide_workspace.py # M6: pure renderer for `.code-workspace` files
44
+ │ ├── preflight_state.py # records preflight result for state machine
45
+ │ ├── reads.py # 4 alias-aware read primitives
46
+ │ ├── review_filter.py # temporal classifier
47
+ │ ├── ship.py # M8: PR open/update orchestrator with cross-repo body links
48
+ │ ├── stash.py # feature-tagged stash save/list/pop
49
+ │ ├── switch.py # WAVE 3.0: slot-model focus primitive (+ --to-slot / --evict-to)
50
+ │ ├── switch_preflight.py # WAVE 3.0: predictable-failure detection for switch
51
+ │ ├── triage.py # cross-repo PR enumeration + priority tiers (slot-enriched)
52
+ │ ├── last_visit.py # per-feature last-visit anchor (visits.json get/mark/reset)
53
+ │ ├── resume.py # feature_resume compound action + resume_summary (counts-only)
54
+ │ ├── thread_actions.py # GH thread resolve/reply wrappers + local resolution log
55
+ │ └── thread_resolutions.py # thread_resolutions.json load/record/filter_since
56
+ ├── agent/
57
+ │ └── runner.py # canopy_run — directory-safe shell exec
58
+ ├── agent_setup/ # ships bundled skills + setup_agent installer
59
+ │ ├── __init__.py # install_skill / install_mcp / check_status
60
+ │ └── skills/ # one SKILL.md per skill name
61
+ │ ├── using-canopy/SKILL.md # default, always installed
62
+ │ └── augment-canopy/SKILL.md # opt-in via --skill augment-canopy (M2)
63
+ ├── integrations/
64
+ │ ├── linear.py # Linear issue fetching (via mcp/client.py)
65
+ │ ├── github.py # GitHub PR + comments (MCP or gh CLI fallback)
66
+ │ └── precommit.py # detect + run pre-commit hooks
67
+ └── mcp/
68
+ ├── server.py # MCP server — 67 tools, stdio transport
69
+ └── client.py # MCP client — stdio + HTTP+OAuth transports
70
+ ```
71
+
72
+ ## Key conventions
73
+
74
+ - **`git/repo.py` is the only module that calls `subprocess.run(["git", ...])`.** Everything else routes through it. Keeps the git layer testable and replaceable.
75
+ - **`mcp/server.py` and `cli/main.py` are thin wrappers.** Business logic lives in `actions/`, `features/coordinator.py`, `git/multi.py`, `workspace/`.
76
+ - **All CLI commands support `--json`.** This is the contract between CLI, MCP, and any GUI. Same JSON shape across surfaces.
77
+ - **Actions return structured errors.** `BlockerError(code, what, expected, actual, fix_actions, details)`. CLI renders via `cli/render.py`; MCP returns `to_dict()`. Same shape, two consumers.
78
+ - **Universal aliases** — every read tool accepts feature name, Linear ID, `<repo>#<n>`, PR URL, `<repo>:<branch>`, or slot id (`worktree-N` → slot's current occupant). Resolved by `actions/aliases.py:resolve_feature` (with single-repo + per-repo-branch fallbacks).
79
+ - **Per-repo branches map** — `FeatureLane.branches: dict[repo, branch]` overrides "branch == feature name" for legacy mismatched-naming features. Use `lane.branch_for(repo)` or `repos_for_feature(workspace, feature)` everywhere — never recompute as `[r for r in feature.repos]` with feature name as branch (regresses Gap 2).
80
+ - **Feature lanes use real Git branches and worktrees.** No virtual branches.
81
+ - **Feature metadata lives in `.canopy/features.json`. Worktrees in `.canopy/worktrees/worktree-N/<repo>/` (generic numbered slots).** A slot holds one feature at a time; a feature's repos sit as siblings inside its slot. Canonical (main repo dirs) is the only place to *run* code; worktrees are passive branch storage.
82
+ - **State files** at `.canopy/state/heads.json` (post-checkout hook output), `.canopy/state/preflight.json` (preflight tracker), `.canopy/state/slots.json` (canonical + warm slot occupancy + `last_touched` LRU map + `in_flight` transaction marker), `.canopy/state/visits.json` (per-feature last-visit anchor: `{feature: {last_visit, previous_visit}}`), and `.canopy/state/thread_resolutions.json` (log of GH review threads canopy itself resolved: `{thread_id: {resolved_by_canopy_at, feature, via_command, via_commit_sha}}`). OAuth tokens at `~/.canopy/mcp-tokens/`.
83
+ - **MCP client supports two transports.** Stdio (existing) for npm/python servers. HTTP+OAuth (new) for hosted servers like Linear's `mcp.linear.app`. Tokens cache per server.
84
+ - **GitHub fallback to gh CLI.** When no `github` MCP server is configured, `integrations/github.py` falls back to `gh api` / `gh pr` for the same return shapes. If neither is available, raises `BlockerError(code='github_not_configured')` with platform-aware install hints.
85
+ - **Single source of truth for state.** `feature_state` uses live git (not heads.json) so it's correct even when the hook hasn't fired. `drift` uses heads.json for the fast cached path.
86
+ - **Feature-aware stash tagging** — `stash save --feature` writes `[canopy <feature> @ <ts>] <message>`. Parser tolerates git's `On <branch>: ` auto-prefix.
87
+
88
+ ## Build & Test
89
+
90
+ ```bash
91
+ pip install -e ".[dev]"
92
+ pytest tests/ -v # 401 tests, ~60s
93
+ ```
94
+
95
+ ## Test Fixtures
96
+
97
+ Tests use real temporary Git repos created in `tests/conftest.py`:
98
+ - `workspace_dir` — bare workspace with `api/` and `ui/` repos on main
99
+ - `workspace_with_feature` — workspace with `auth-flow` branches + commits in both repos
100
+ - `canopy_toml` — workspace with a canopy.toml already written
101
+
102
+ For integration testing against real services, see `~/projects/canopy-test/` (memory: project_test_workspace).
103
+
104
+ ## Important Implementation Details
105
+
106
+ - **Python 3.10+ compat:** `tomli` on 3.10, `tomllib` on 3.11+. See `config.py`.
107
+ - **Drift detection:** post-checkout hook installed by `canopy init` (or `canopy hooks install`). Hook is Python; uses `fcntl.flock` + atomic rename so concurrent fires across repos don't race. Respects `core.hooksPath` (Husky-friendly). Chains pre-existing user hooks. Worktrees inherit hooks via `commondir` resolution.
108
+ - **`--no-track` on branch creation:** `git/repo.py:create_branch` and `worktree_add` always pass `--no-track` so a `branch.autoSetupMerge=inherit` gitconfig doesn't accidentally set the new branch's upstream to `dev`.
109
+ - **Slot limits:** `[workspace] slots = N` in canopy.toml caps the number of warm slots (default **2**, so 1 canonical + 2 warm = 3 live trees max). The pre-3.0 `max_worktrees` key now raises `ConfigError` pointing at `canopy migrate-slots`. See `actions/switch_preflight.py:warm_slot_cap`.
110
+ - **Action contract:** `actions/protocol.py` (planned) will formalize the per-repo `{status, before, after, reason?}` shape. For now, each action returns it ad-hoc.
111
+ - **Skill bundling:** Bundled skills live at `src/canopy/agent_setup/skills/<name>/SKILL.md`. `canopy setup-agent` copies them to `~/.claude/skills/<name>/SKILL.md`. The default `using-canopy` skill always installs; opt-in extras (e.g. `augment-canopy`) install via `--skill <name>` (repeatable). Foreign skills with the same path are not overwritten without `--reinstall`. The `_SKILL_SOURCE` constant remains as a backward-compat alias pointing at `using-canopy`'s source.
112
+ - **Version bumps:** When shipping a milestone, bump `__version__` in [`src/canopy/__init__.py`](src/canopy/__init__.py) and add a section to [`CHANGELOG.md`](CHANGELOG.md). The version handshake (`canopy --version`, `mcp__canopy__version`, doctor's `cli_stale` / `mcp_stale` checks) is only useful when this number actually moves — drift was the bug 0.5.0 caught.
113
+
114
+ ## MCP Server (67 tools)
115
+
116
+ Grouped by topic. Run with `canopy-mcp` (entry point) or `python -m canopy.mcp.server`.
117
+
118
+ ```
119
+ Meta: version, doctor # 21-code / 12-category recovery primitive
120
+ Workspace: workspace_status, workspace_context, workspace_config, workspace_reinit
121
+ Feature: feature_create, feature_list, feature_status, feature_diff,
122
+ feature_changes, feature_merge_readiness, feature_paths, feature_done,
123
+ feature_link_linear, feature_state
124
+ Slots: slots, slot_load, slot_clear, slot_swap, migrate_slots # WAVE 3.0
125
+ Actions: switch, triage, drift, conflicts # switch is the slot-model focus primitive
126
+ Reads: linear_get_issue, github_get_pr, github_get_branch, github_get_pr_comments,
127
+ linear_my_issues, pr_checks # pr_checks = M10 CI rollup
128
+ Workflow: ship, draft_replies, feature_resume # M8 + M9 + Plan 2 — capstone + reply drafts + session resume
129
+ Threads: resolve_thread, reply_to_thread # Plan 2 — GH review thread mutations + local log
130
+ Run/Pre: run, preflight, review_status, review_comments, review_prep
131
+ Stash: stash_save_feature, stash_list_grouped, stash_pop_feature,
132
+ stash_save, stash_pop, stash_list, stash_drop
133
+ Worktree: worktree_create, worktree_info, worktree_bootstrap # bootstrap = M6
134
+ Branch: branch_list, branch_delete, branch_rename
135
+ Misc: log, checkout, sync
136
+ ```
137
+
138
+ ## MCP Client
139
+
140
+ Two transports.
141
+
142
+ **stdio** for npm/python servers:
143
+ ```json
144
+ { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"],
145
+ "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."} } }
146
+ ```
147
+
148
+ **HTTP + OAuth** for hosted servers like Linear:
149
+ ```json
150
+ { "linear": { "type": "http", "url": "https://mcp.linear.app/mcp", "oauth": true } }
151
+ ```
152
+
153
+ Token cache at `~/.canopy/mcp-tokens/<server>.{client,tokens}.json`. First call opens browser; subsequent calls silent.
154
+
155
+ ## When working in this repo
156
+
157
+ - Read `docs/concepts.md` if you need the action framework / state machine vocabulary.
158
+ - Read `docs/agents.md` if you're implementing or using the agent integration.
159
+ - New actions: stub in `src/canopy/actions/`, raise `BlockerError` for preconditions, expose via CLI in `cli/main.py` + MCP in `mcp/server.py`. Add tests in `tests/test_<action>.py` using the existing `workspace_with_feature` fixture.
160
+ - New MCP tools: register an existing `actions/*.py` function under `@mcp.tool()` in `mcp/server.py`. Update `docs/mcp.md` and `docs/agents.md`.
161
+ - New CLI commands: define a handler `cmd_<name>(args)`, add a subparser in `main()`, dispatch in the `commands` dict. Update `docs/commands.md`.
162
+ - Adding a new ⨯ tool to canopy → also update `~/.claude/skills/using-canopy/SKILL.md` and `src/canopy/agent_setup/skills/using-canopy/SKILL.md` so the agent learns when to prefer it.