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.
- canopy_cli-3.1.0/.github/workflows/ci.yml +76 -0
- canopy_cli-3.1.0/.github/workflows/publish.yml +36 -0
- canopy_cli-3.1.0/.gitignore +17 -0
- canopy_cli-3.1.0/AGENTS.md +228 -0
- canopy_cli-3.1.0/CHANGELOG.md +115 -0
- canopy_cli-3.1.0/CLAUDE.md +162 -0
- canopy_cli-3.1.0/PKG-INFO +282 -0
- canopy_cli-3.1.0/README.md +254 -0
- canopy_cli-3.1.0/docs/agents.md +249 -0
- canopy_cli-3.1.0/docs/architecture/providers.md +372 -0
- canopy_cli-3.1.0/docs/architecture.md +321 -0
- canopy_cli-3.1.0/docs/archive/test-plan.md +326 -0
- canopy_cli-3.1.0/docs/canopy-banner.svg +95 -0
- canopy_cli-3.1.0/docs/cli-commit.svg +97 -0
- canopy_cli-3.1.0/docs/cli-config.svg +72 -0
- canopy_cli-3.1.0/docs/cli-done.svg +115 -0
- canopy_cli-3.1.0/docs/cli-drift.svg +103 -0
- canopy_cli-3.1.0/docs/cli-init.svg +128 -0
- canopy_cli-3.1.0/docs/cli-list.svg +114 -0
- canopy_cli-3.1.0/docs/cli-push.svg +90 -0
- canopy_cli-3.1.0/docs/cli-review.svg +136 -0
- canopy_cli-3.1.0/docs/cli-setup-agent.svg +75 -0
- canopy_cli-3.1.0/docs/cli-stage.svg +82 -0
- canopy_cli-3.1.0/docs/cli-state.svg +130 -0
- canopy_cli-3.1.0/docs/cli-status.svg +129 -0
- canopy_cli-3.1.0/docs/cli-switch.svg +117 -0
- canopy_cli-3.1.0/docs/cli-triage.svg +124 -0
- canopy_cli-3.1.0/docs/cli-worktree.svg +142 -0
- canopy_cli-3.1.0/docs/commands.md +211 -0
- canopy_cli-3.1.0/docs/concepts.md +277 -0
- canopy_cli-3.1.0/docs/mcp.md +217 -0
- canopy_cli-3.1.0/docs/workspace.md +329 -0
- canopy_cli-3.1.0/gen_svgs.py +284 -0
- canopy_cli-3.1.0/mockups/canopy-vscode.html +842 -0
- canopy_cli-3.1.0/pyproject.toml +55 -0
- canopy_cli-3.1.0/src/canopy/__init__.py +2 -0
- canopy_cli-3.1.0/src/canopy/actions/__init__.py +32 -0
- canopy_cli-3.1.0/src/canopy/actions/aliases.py +421 -0
- canopy_cli-3.1.0/src/canopy/actions/augments.py +55 -0
- canopy_cli-3.1.0/src/canopy/actions/bootstrap.py +249 -0
- canopy_cli-3.1.0/src/canopy/actions/bot_resolutions.py +123 -0
- canopy_cli-3.1.0/src/canopy/actions/bot_status.py +133 -0
- canopy_cli-3.1.0/src/canopy/actions/commit.py +511 -0
- canopy_cli-3.1.0/src/canopy/actions/conflicts.py +314 -0
- canopy_cli-3.1.0/src/canopy/actions/doctor.py +1459 -0
- canopy_cli-3.1.0/src/canopy/actions/draft_replies.py +185 -0
- canopy_cli-3.1.0/src/canopy/actions/drift.py +241 -0
- canopy_cli-3.1.0/src/canopy/actions/errors.py +115 -0
- canopy_cli-3.1.0/src/canopy/actions/evacuate.py +192 -0
- canopy_cli-3.1.0/src/canopy/actions/feature_state.py +607 -0
- canopy_cli-3.1.0/src/canopy/actions/historian.py +612 -0
- canopy_cli-3.1.0/src/canopy/actions/ide_workspace.py +49 -0
- canopy_cli-3.1.0/src/canopy/actions/last_visit.py +83 -0
- canopy_cli-3.1.0/src/canopy/actions/migrate_slots.py +313 -0
- canopy_cli-3.1.0/src/canopy/actions/preflight_state.py +97 -0
- canopy_cli-3.1.0/src/canopy/actions/push.py +199 -0
- canopy_cli-3.1.0/src/canopy/actions/reads.py +304 -0
- canopy_cli-3.1.0/src/canopy/actions/resume.py +582 -0
- canopy_cli-3.1.0/src/canopy/actions/review_filter.py +135 -0
- canopy_cli-3.1.0/src/canopy/actions/ship.py +399 -0
- canopy_cli-3.1.0/src/canopy/actions/slot_details.py +208 -0
- canopy_cli-3.1.0/src/canopy/actions/slot_load.py +383 -0
- canopy_cli-3.1.0/src/canopy/actions/slots.py +221 -0
- canopy_cli-3.1.0/src/canopy/actions/stash.py +230 -0
- canopy_cli-3.1.0/src/canopy/actions/switch.py +775 -0
- canopy_cli-3.1.0/src/canopy/actions/switch_preflight.py +192 -0
- canopy_cli-3.1.0/src/canopy/actions/thread_actions.py +88 -0
- canopy_cli-3.1.0/src/canopy/actions/thread_resolutions.py +101 -0
- canopy_cli-3.1.0/src/canopy/actions/triage.py +286 -0
- canopy_cli-3.1.0/src/canopy/agent/__init__.py +5 -0
- canopy_cli-3.1.0/src/canopy/agent/runner.py +129 -0
- canopy_cli-3.1.0/src/canopy/agent_setup/__init__.py +264 -0
- canopy_cli-3.1.0/src/canopy/agent_setup/skills/augment-canopy/SKILL.md +116 -0
- canopy_cli-3.1.0/src/canopy/agent_setup/skills/using-canopy/SKILL.md +191 -0
- canopy_cli-3.1.0/src/canopy/cli/__init__.py +0 -0
- canopy_cli-3.1.0/src/canopy/cli/main.py +4152 -0
- canopy_cli-3.1.0/src/canopy/cli/render.py +98 -0
- canopy_cli-3.1.0/src/canopy/cli/ui.py +150 -0
- canopy_cli-3.1.0/src/canopy/features/__init__.py +2 -0
- canopy_cli-3.1.0/src/canopy/features/coordinator.py +1256 -0
- canopy_cli-3.1.0/src/canopy/git/__init__.py +0 -0
- canopy_cli-3.1.0/src/canopy/git/hooks.py +173 -0
- canopy_cli-3.1.0/src/canopy/git/multi.py +435 -0
- canopy_cli-3.1.0/src/canopy/git/repo.py +859 -0
- canopy_cli-3.1.0/src/canopy/git/templates/post-checkout.py +67 -0
- canopy_cli-3.1.0/src/canopy/graph/__init__.py +0 -0
- canopy_cli-3.1.0/src/canopy/integrations/__init__.py +0 -0
- canopy_cli-3.1.0/src/canopy/integrations/github.py +983 -0
- canopy_cli-3.1.0/src/canopy/integrations/linear.py +307 -0
- canopy_cli-3.1.0/src/canopy/integrations/precommit.py +239 -0
- canopy_cli-3.1.0/src/canopy/mcp/__init__.py +0 -0
- canopy_cli-3.1.0/src/canopy/mcp/client.py +329 -0
- canopy_cli-3.1.0/src/canopy/mcp/server.py +1797 -0
- canopy_cli-3.1.0/src/canopy/providers/__init__.py +105 -0
- canopy_cli-3.1.0/src/canopy/providers/github_issues.py +289 -0
- canopy_cli-3.1.0/src/canopy/providers/linear.py +341 -0
- canopy_cli-3.1.0/src/canopy/providers/types.py +149 -0
- canopy_cli-3.1.0/src/canopy/workspace/__init__.py +4 -0
- canopy_cli-3.1.0/src/canopy/workspace/config.py +378 -0
- canopy_cli-3.1.0/src/canopy/workspace/context.py +224 -0
- canopy_cli-3.1.0/src/canopy/workspace/discovery.py +197 -0
- canopy_cli-3.1.0/src/canopy/workspace/workspace.py +173 -0
- canopy_cli-3.1.0/tests/__init__.py +0 -0
- canopy_cli-3.1.0/tests/conftest.py +221 -0
- canopy_cli-3.1.0/tests/test_action_errors.py +215 -0
- canopy_cli-3.1.0/tests/test_agent_setup.py +206 -0
- canopy_cli-3.1.0/tests/test_aliases.py +311 -0
- canopy_cli-3.1.0/tests/test_augments.py +108 -0
- canopy_cli-3.1.0/tests/test_bootstrap.py +210 -0
- canopy_cli-3.1.0/tests/test_bot_resolutions.py +133 -0
- canopy_cli-3.1.0/tests/test_bot_status.py +159 -0
- canopy_cli-3.1.0/tests/test_ci_status.py +148 -0
- canopy_cli-3.1.0/tests/test_commit.py +657 -0
- canopy_cli-3.1.0/tests/test_commit_push_integration.py +105 -0
- canopy_cli-3.1.0/tests/test_config.py +274 -0
- canopy_cli-3.1.0/tests/test_config_done.py +287 -0
- canopy_cli-3.1.0/tests/test_conflicts.py +155 -0
- canopy_cli-3.1.0/tests/test_context_stage.py +319 -0
- canopy_cli-3.1.0/tests/test_coordinator.py +381 -0
- canopy_cli-3.1.0/tests/test_doctor.py +896 -0
- canopy_cli-3.1.0/tests/test_draft_replies.py +162 -0
- canopy_cli-3.1.0/tests/test_drift.py +266 -0
- canopy_cli-3.1.0/tests/test_evacuate.py +179 -0
- canopy_cli-3.1.0/tests/test_feature_state.py +391 -0
- canopy_cli-3.1.0/tests/test_feature_state_bot.py +209 -0
- canopy_cli-3.1.0/tests/test_historian.py +322 -0
- canopy_cli-3.1.0/tests/test_hooks.py +227 -0
- canopy_cli-3.1.0/tests/test_last_visit.py +68 -0
- canopy_cli-3.1.0/tests/test_linear_integration.py +558 -0
- canopy_cli-3.1.0/tests/test_mcp_server_worktree_create.py +189 -0
- canopy_cli-3.1.0/tests/test_migrate_slots.py +128 -0
- canopy_cli-3.1.0/tests/test_new_commands.py +396 -0
- canopy_cli-3.1.0/tests/test_precommit.py +97 -0
- canopy_cli-3.1.0/tests/test_providers_github_issues.py +274 -0
- canopy_cli-3.1.0/tests/test_providers_linear.py +281 -0
- canopy_cli-3.1.0/tests/test_providers_registry.py +123 -0
- canopy_cli-3.1.0/tests/test_providers_types.py +78 -0
- canopy_cli-3.1.0/tests/test_push.py +230 -0
- canopy_cli-3.1.0/tests/test_reads.py +284 -0
- canopy_cli-3.1.0/tests/test_repo.py +329 -0
- canopy_cli-3.1.0/tests/test_resume.py +1694 -0
- canopy_cli-3.1.0/tests/test_review.py +417 -0
- canopy_cli-3.1.0/tests/test_review_filter.py +193 -0
- canopy_cli-3.1.0/tests/test_runner.py +143 -0
- canopy_cli-3.1.0/tests/test_ship.py +130 -0
- canopy_cli-3.1.0/tests/test_slot_details.py +45 -0
- canopy_cli-3.1.0/tests/test_slot_load.py +139 -0
- canopy_cli-3.1.0/tests/test_slots.py +172 -0
- canopy_cli-3.1.0/tests/test_stash_features.py +250 -0
- canopy_cli-3.1.0/tests/test_switch.py +333 -0
- canopy_cli-3.1.0/tests/test_switch_action.py +347 -0
- canopy_cli-3.1.0/tests/test_switch_preflight.py +47 -0
- canopy_cli-3.1.0/tests/test_thread_actions.py +271 -0
- canopy_cli-3.1.0/tests/test_thread_graphql.py +160 -0
- canopy_cli-3.1.0/tests/test_triage.py +377 -0
- canopy_cli-3.1.0/tests/test_workspace.py +113 -0
- canopy_cli-3.1.0/tests/test_worktree_features.py +318 -0
- canopy_cli-3.1.0/vscode-extension/.gitignore +5 -0
- canopy_cli-3.1.0/vscode-extension/.vscodeignore +14 -0
- canopy_cli-3.1.0/vscode-extension/CHANGELOG.md +128 -0
- canopy_cli-3.1.0/vscode-extension/LICENSE +21 -0
- canopy_cli-3.1.0/vscode-extension/README.md +82 -0
- canopy_cli-3.1.0/vscode-extension/esbuild.config.mjs +71 -0
- canopy_cli-3.1.0/vscode-extension/jest.config.ts +22 -0
- canopy_cli-3.1.0/vscode-extension/media/canopy-icon.png +0 -0
- canopy_cli-3.1.0/vscode-extension/media/canopy-icon.svg +588 -0
- canopy_cli-3.1.0/vscode-extension/media/screenshots/dashboard-feature.png +0 -0
- canopy_cli-3.1.0/vscode-extension/media/screenshots/dashboard-global.png +0 -0
- canopy_cli-3.1.0/vscode-extension/package-lock.json +9054 -0
- canopy_cli-3.1.0/vscode-extension/package.json +297 -0
- canopy_cli-3.1.0/vscode-extension/src/__mocks__/vscode.ts +98 -0
- canopy_cli-3.1.0/vscode-extension/src/canopyCli.test.ts +196 -0
- canopy_cli-3.1.0/vscode-extension/src/canopyCli.ts +862 -0
- canopy_cli-3.1.0/vscode-extension/src/canopyClient.ts +704 -0
- canopy_cli-3.1.0/vscode-extension/src/cliResolver.test.ts +122 -0
- canopy_cli-3.1.0/vscode-extension/src/cliResolver.ts +134 -0
- canopy_cli-3.1.0/vscode-extension/src/commands/createFeature.ts +161 -0
- canopy_cli-3.1.0/vscode-extension/src/commands/createFeatureFromIssue.ts +85 -0
- canopy_cli-3.1.0/vscode-extension/src/commands/installBackend.ts +218 -0
- canopy_cli-3.1.0/vscode-extension/src/commands/setupWizard.ts +103 -0
- canopy_cli-3.1.0/vscode-extension/src/extension.ts +828 -0
- canopy_cli-3.1.0/vscode-extension/src/mcpResolver.ts +130 -0
- canopy_cli-3.1.0/vscode-extension/src/stateReader.test.ts +273 -0
- canopy_cli-3.1.0/vscode-extension/src/stateReader.ts +287 -0
- canopy_cli-3.1.0/vscode-extension/src/statusBar.ts +96 -0
- canopy_cli-3.1.0/vscode-extension/src/types.ts +192 -0
- canopy_cli-3.1.0/vscode-extension/src/views/GlobalDashboardPanel.ts +1020 -0
- canopy_cli-3.1.0/vscode-extension/src/views/canopyTreeProvider.ts +269 -0
- canopy_cli-3.1.0/vscode-extension/src/views/themeShim.ts +113 -0
- canopy_cli-3.1.0/vscode-extension/src/watchers.ts +79 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/cockpitPanel.ts +395 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/branchLedger.ts +72 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/bridge.ts +72 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/capReachedModal.ts +112 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/focusTile.ts +139 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/newFeatureForm.ts +174 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/styles.ts +691 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/triageFeed.ts +91 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/util.ts +40 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/components/worktreeRow.ts +71 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/dashboardPanel.ts +904 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/Dashboard.tsx +137 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/FeatureView.tsx +972 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/GlobalView.tsx +649 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/Skeletons.tsx +38 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/diff.ts +118 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/index.tsx +17 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/protocol.ts +129 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/global-dashboard/vscode.ts +30 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/newFeaturePanel.ts +322 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/shared/pastel.css +801 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/themes/index.ts +42 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/themes/minimal.ts +62 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/themes/navy.ts +60 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/themes/render.ts +96 -0
- canopy_cli-3.1.0/vscode-extension/src/webview/themes/types.ts +104 -0
- 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,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.
|