Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38a56f2
feat(github): GraphQL thread API — list/resolve/unresolve/reply + thr…
ashmitb95 May 30, 2026
6ceed0d
feat(actions): canopy resolve <thread-id> — close + log
ashmitb95 May 30, 2026
1b1034e
feat(actions): canopy reply <thread-id> --body <…> [--resolve]
ashmitb95 May 30, 2026
b65b92d
feat(commit): --address optionally resolves the GH thread
ashmitb95 May 30, 2026
c2ad0c3
fix(milestone-1): author_type from __typename + resolve_after failure…
ashmitb95 May 30, 2026
422c598
feat(actions): last_visit state — per-feature visit anchor
ashmitb95 May 30, 2026
9a66523
feat(resume): switch-aware compound action + intent hints
ashmitb95 May 30, 2026
b5597b6
feat(resume): populate commits-since-last-visit per repo (T7)
ashmitb95 May 30, 2026
78c4c8e
feat(resume): thread delta — new + resolved-on-GH + resolved-by-canopy
ashmitb95 May 30, 2026
1fef8a0
feat(resume): current_state.feature_state + ci_summary + branch_position
ashmitb95 May 30, 2026
fb7c41e
feat(resume): current_state.bot_unresolved_total
ashmitb95 May 30, 2026
6705995
feat(resume): draft_replies_summary + draft_replies_pending (T11)
ashmitb95 May 30, 2026
dc78643
fix(milestone-2): linear from lane + open_thread_count rollup + hint …
ashmitb95 May 30, 2026
27983ef
feat(resume): historian session excerpts since last visit
ashmitb95 May 30, 2026
c5c3ea8
feat(switch): bump last_visit on every switch-into-feature
ashmitb95 May 30, 2026
7863a51
feat(switch): embed since_last_visit_summary in switch return
ashmitb95 May 30, 2026
77dd64a
feat(cli): canopy resume <alias>
ashmitb95 May 30, 2026
09ad50e
feat(mcp): feature_resume tool
ashmitb95 May 30, 2026
51f8287
docs(skill): teach feature_resume as session-start primitive
ashmitb95 May 30, 2026
915c297
docs: feature_resume + thread mutations + state files + 3.1.0
ashmitb95 May 30, 2026
1dbfe94
fix(milestone-3): doc shape fidelity + augment-canopy + cross-repo + …
ashmitb95 May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,58 @@ Tracks the Python side (CLI + MCP server). The VSCode extension has its own [vsc

Versions follow semver. Pre-1.0 — minor bumps may add features or break behavior; the README is the source-of-truth contract.

## 3.1.0 — 2026-05-30 (Plan 2 — Feature Resume)

### Added
- `canopy resume <alias>` (+ `mcp__canopy__feature_resume`): switch-aware
compound action. One call: alias → switch-if-needed → refresh GitHub + Linear →
compute structured brief with `intent_hints` for the most likely next actions.
See `docs/concepts.md#returning-to-a-feature`.
- `canopy resolve <thread_id>` (+ `mcp__canopy__resolve_thread`): close a
GitHub review thread + log to `.canopy/state/thread_resolutions.json` for
attribution in the resume brief.
- `canopy reply <thread_id> [--body | --body-file | stdin]`
(+ `mcp__canopy__reply_to_thread`): post a reply to a GH review thread.
`--resolve` (or `resolve_after=True`) closes the thread after posting.
- `canopy commit --address <id> --resolve-thread`: optionally close the GH
review thread after the local commit. Augment
`auto_resolve_threads_on_address = true` in canopy.toml makes this the
default for the workspace. `--no-resolve-thread` overrides the augment
per-invocation.
- New state files: `.canopy/state/visits.json` (per-feature last-visit anchor
`{feature: {last_visit, previous_visit}}`); `.canopy/state/thread_resolutions.json`
(canopy-driven GH thread closures `{thread_id: {resolved_by_canopy_at,
feature, via_command, via_commit_sha}}`).
- `actions/last_visit.py` — get/mark/reset the per-feature visit anchor.
- `actions/resume.py` — `feature_resume` compound action + `resume_summary`
(counts-only view embedded in `switch` return).
- `actions/thread_actions.py` — `resolve_thread` + `reply_to_thread` wrappers
+ local resolution log writer.
- `actions/thread_resolutions.py` — load/record/filter_since for the
thread-resolutions log.
- GraphQL thread API in `integrations/github.py`: `list_review_threads`,
`resolve_thread`, `unresolve_thread`, `reply_to_thread`. Every comment from
`get_review_comments` now carries a `thread_id` field (GraphQL-sourced when
available, `""` on REST fallback) and `author_type` from GraphQL `__typename`.
- Bundled `using-canopy` skill now teaches `feature_resume` as the
session-start primitive and documents the "Closing out review threads"
workflow.

### Changed
- `switch(feature)` bumps `last_visit` on every successful switch and embeds
`since_last_visit_summary` in its return value — a counts-only view
(commits, threads, GH resolutions, draft replies) so the agent sees
"something changed" without a full `feature_resume` round-trip. Sets
`degraded: true` if GitHub is unreachable.
- `get_review_comments` prefers GraphQL when available (single round-trip for
thread IDs + `author_type`); falls back to REST with `thread_id=""`.

### Notes
- `feature_resume` refreshes GitHub + Linear on every call — the brief is
never cached at the canopy layer.
- Plan 1's slot model is the prerequisite. If upgrading from pre-3.0, run
`canopy migrate-slots` first.

## 3.0.0 — 2026-05-28 (Wave 3.0)

**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.
Expand Down
15 changes: 10 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ src/canopy/
│ ├── stash.py # feature-tagged stash save/list/pop
│ ├── switch.py # WAVE 3.0: slot-model focus primitive (+ --to-slot / --evict-to)
│ ├── switch_preflight.py # WAVE 3.0: predictable-failure detection for switch
│ └── triage.py # cross-repo PR enumeration + priority tiers (slot-enriched)
│ ├── triage.py # cross-repo PR enumeration + priority tiers (slot-enriched)
│ ├── last_visit.py # per-feature last-visit anchor (visits.json get/mark/reset)
│ ├── resume.py # feature_resume compound action + resume_summary (counts-only)
│ ├── thread_actions.py # GH thread resolve/reply wrappers + local resolution log
│ └── thread_resolutions.py # thread_resolutions.json load/record/filter_since
├── agent/
│ └── runner.py # canopy_run — directory-safe shell exec
├── agent_setup/ # ships bundled skills + setup_agent installer
Expand All @@ -61,7 +65,7 @@ src/canopy/
│ ├── github.py # GitHub PR + comments (MCP or gh CLI fallback)
│ └── precommit.py # detect + run pre-commit hooks
└── mcp/
├── server.py # MCP server — 59 tools, stdio transport
├── server.py # MCP server — 67 tools, stdio transport
└── client.py # MCP client — stdio + HTTP+OAuth transports
```

Expand All @@ -75,7 +79,7 @@ src/canopy/
- **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).
- **Feature lanes use real Git branches and worktrees.** No virtual branches.
- **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.
- **State files** at `.canopy/state/heads.json` (post-checkout hook output), `.canopy/state/preflight.json` (preflight tracker), and `.canopy/state/slots.json` (canonical + warm slot occupancy + `last_touched` LRU map + `in_flight` transaction marker). OAuth tokens at `~/.canopy/mcp-tokens/`.
- **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/`.
- **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.
- **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.
- **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.
Expand Down Expand Up @@ -107,7 +111,7 @@ For integration testing against real services, see `~/projects/canopy-test/` (me
- **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.
- **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.

## MCP Server (64 tools)
## MCP Server (67 tools)

Grouped by topic. Run with `canopy-mcp` (entry point) or `python -m canopy.mcp.server`.

Expand All @@ -121,7 +125,8 @@ Slots: slots, slot_load, slot_clear, slot_swap, migrate_slots # WAVE 3.
Actions: switch, triage, drift, conflicts # switch is the slot-model focus primitive
Reads: linear_get_issue, github_get_pr, github_get_branch, github_get_pr_comments,
linear_my_issues, pr_checks # pr_checks = M10 CI rollup
Workflow: ship, draft_replies # M8 + M9 — capstone + addressed-comment drafts
Workflow: ship, draft_replies, feature_resume # M8 + M9 + Plan 2 — capstone + reply drafts + session resume
Threads: resolve_thread, reply_to_thread # Plan 2 — GH review thread mutations + local log
Run/Pre: run, preflight, review_status, review_comments, review_prep
Stash: stash_save_feature, stash_list_grouped, stash_pop_feature,
stash_save, stash_pop, stash_list, stash_drop
Expand Down
24 changes: 21 additions & 3 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ How AI coding agents (Claude Code primarily; others by analogy) integrate with c

Three pieces, all installed in one step by `canopy init`:

1. **Canopy MCP server** (`canopy-mcp` binary) — 64 tools exposing every canopy operation. Registered in `<workspace>/.mcp.json`.
1. **Canopy MCP server** (`canopy-mcp` binary) — 67 tools exposing every canopy operation. Registered in `<workspace>/.mcp.json`.
2. **`using-canopy` skill** at `~/.claude/skills/using-canopy/SKILL.md` — tells the agent *when* to prefer canopy MCP over raw bash.
3. **Per-workspace MCP config** in `<workspace>/.mcp.json` with `CANOPY_ROOT` set so the server scopes to the right workspace.

Expand Down Expand Up @@ -54,6 +54,7 @@ The skill encodes this matrix; the agent reads it on session start. Mirror here

| What you want | Canopy tool | Don't use |
|---|---|---|
| Return to a feature in a new session | `mcp__canopy__feature_resume` | `switch` + `feature_state` + `github_get_pr_comments` separately |
| What feature should I work on? | `mcp__canopy__triage` | per-repo `gh pr list` + manual grouping |
| Show me everything about a feature | `mcp__canopy__feature_state` | composing many reads |
| Switch a feature into main (the focus primitive) | `mcp__canopy__switch` | `cd repo && git checkout`, or guessing paths |
Expand All @@ -72,6 +73,8 @@ The skill encodes this matrix; the agent reads it on session start. Mirror here
| Free a slot without bringing a new feature in | `mcp__canopy__slot_clear(slot_id)` | manual stash + branch checkout |
| Exchange two slots' occupants (e.g., shuffle warm order) | `mcp__canopy__slot_swap(slot_a, slot_b)` | two `slot_load` calls with `replace=True` |
| Migrate a pre-3.0 workspace to the slot model | `mcp__canopy__migrate_slots` | hand-renaming `.canopy/worktrees/` dirs |
| Close a GitHub review thread + log it | `mcp__canopy__resolve_thread(thread_id)` | raw GraphQL or `gh api` |
| Reply to a GitHub review thread | `mcp__canopy__reply_to_thread(thread_id, body)` | raw GraphQL or `gh pr review` |

### Vocabulary note: hibernate ⇄ release_current

Expand All @@ -86,11 +89,26 @@ Same operation. Different surface vocab. A future canopy release may add `hibern

A feature in the resulting state is **hibernating** (synonyms in the wild: "branch only", "released to cold", "wound down" — all the same thing).

## Session start — returning to a feature

When opening a new session on a feature you've worked on before, call `feature_resume` first:

```python
mcp__canopy__feature_resume(alias="SIN-12-search")
# → switches if needed, refreshes GH + Linear, returns brief + intent_hints
# → bumps the last-visit anchor so the next resume gets an accurate delta
```

The brief tells you: what state the feature is in, what changed since you last visited (commits, new/resolved threads), and `intent_hints` for the most likely next action categories. It replaces the manual `switch → feature_state → github_get_pr_comments` chain at session start.

The bundled `using-canopy` skill's "Session start" section encodes this as the expected agent protocol. See [concepts.md §5](concepts.md#5-returning-to-a-feature--the-resume-brief) for the full brief shape and freshness policy.

## The daily loop

```
1. triage() → pick a feature from the prioritized list
2. feature_state(feature) → get current state + next_actions
1. feature_resume(feature) → session-start brief + switch-if-needed (Plan 2)
OR triage() → if you don't know what to work on
2. feature_state(feature) → get current state (brief.current_state has feature_state + intent_hints)
3. follow next_actions[0] → primary CTA (canopy decided what to do next)
4. feature_state again → confirm state advanced
5. repeat
Expand Down
14 changes: 13 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Write actions and execution.

| Command | What it does |
|---|---|
| `canopy resume <alias> [--reset-anchor]` | **Session-start primitive (Plan 2).** Switch-aware compound action: alias → switch-if-needed → refresh GitHub + Linear → compute structured brief → bump last-visit anchor. Returns `{feature, switch_performed, first_visit, window_hours, since_last_visit, current_state, next_actions, intent_hints}`. Use this instead of manually calling `switch` + `feature_state` + `github_get_pr_comments` at the start of a session. `--reset-anchor` sets the anchor to now (useful when you want a fresh delta window). See [concepts.md §5](concepts.md#5-returning-to-a-feature--the-resume-brief). |
| `canopy resolve <thread_id> [--feature <f>]` | **Plan 2.** Resolve a GitHub PR review thread via GraphQL + record the closure in `.canopy/state/thread_resolutions.json`. The log feeds `feature_resume`'s `since_last_visit.resolved_threads` count. `--feature` pins which feature the resolution is attributed to (defaults to the canonical feature). |
| `canopy reply <thread_id> [--body <text> \| --body-file <path> \| stdin] [--resolve] [--feature <f>]` | **Plan 2.** Post a reply to a GitHub review thread. Body comes from `--body`, `--body-file`, or stdin (pipe-friendly). `--resolve` closes the thread after posting (equivalent to `reply_to_thread(..., resolve_after=True)`) and logs the closure. |
| `canopy switch <feature> [--release-current] [--no-evict] [--evict <f>] [--evict-to <slot-N>] [--to-slot <slot-N>]` | **The focus primitive (Wave 3.0 slot model).** Promote a feature to the canonical slot. Default (active rotation): previously-canonical evacuates into a warm slot (full stash → checkout → pop). When the destination is already warm, the swap is a fast 5-op-per-repo dance — no `mv`, no slot renaming. `--release-current` (wind-down): previous goes cold with a feature-tagged stash. `--evict-to <slot-N>` pins which slot the outgoing canonical lands in. `--to-slot <slot-N>` promotes whatever feature already occupies that slot (omit `<feature>`). Cap-reached blocker surfaces explicit fix actions (wind-down, evict a specific slot, raise cap). See [docs/concepts.md §4](concepts.md#4-the-slot-model). |
| `canopy slot load <feature> [<slot-N>] [--replace] [--bootstrap]` | **Wave 3.0.** Warm a cold feature into a slot WITHOUT changing canonical. `<slot-N>` defaults to the lowest free slot. `--replace` evicts the slot's current occupant to cold first. `--bootstrap` runs the env-file copy + install_cmd + IDE workspace gen (same as `canopy worktree-bootstrap`). The feature must already be registered — create it with `canopy feature create` first. |
| `canopy slot clear <slot-N>` | **Wave 3.0.** Evict that slot's occupant to cold with a feature-tagged stash if dirty. The slot id stays — only the occupant moves. |
Expand All @@ -57,7 +60,7 @@ Write actions and execution.
| `canopy run <repo> <command> [--feature]` | Run a shell command in a canopy-managed repo with cwd resolved internally. The "agent never `cd`s" tool — also useful from a CLI in a deeply nested directory. |
| `canopy code\|cursor\|fork <feature\|.>` | Open the feature in VS Code / Cursor / Fork.app (alias-aware; generates `.code-workspace` for the IDE ones). |
| `canopy sync` | Pull default branch + rebase feature branches across repos. |
| `canopy commit -m <msg> [--feature <f>] [--repo <r,...>] [--paths <p ...>] [--no-hooks] [--amend] [--address <id>]` | **Wave 2.3 + M3.** Commit across every repo in the canonical (or named) feature with a single message. Pre-flight refuses with `BlockerError(code='wrong_branch')` if any in-scope repo has drifted; per-repo hook failures don't cancel the others (status: `hooks_failed`). `--address <comment-id>` (numeric id or GitHub URL) auto-suffixes the message with the bot comment's title + URL and records the resolution in `.canopy/state/bot_resolutions.json`. Non-bot comments raise `BlockerError(code='not_a_bot_comment')`. |
| `canopy commit -m <msg> [--feature <f>] [--repo <r,...>] [--paths <p ...>] [--no-hooks] [--amend] [--address <id>] [--resolve-thread \| --no-resolve-thread]` | **Wave 2.3 + M3 + Plan 2.** Commit across every repo in the canonical (or named) feature with a single message. Pre-flight refuses with `BlockerError(code='wrong_branch')` if any in-scope repo has drifted; per-repo hook failures don't cancel the others (status: `hooks_failed`). `--address <comment-id>` (numeric id or GitHub URL) auto-suffixes the message with the bot comment's title + URL and records the resolution in `.canopy/state/bot_resolutions.json`. Non-bot comments raise `BlockerError(code='not_a_bot_comment')`. `--resolve-thread` additionally closes the corresponding GitHub review thread and logs it to `thread_resolutions.json`. `--no-resolve-thread` disables this even when `[augments] auto_resolve_threads_on_address = true` is set in canopy.toml. |
| `canopy bot-status [--feature <f>] [--unresolved-only]` | **M3.** Per-feature rollup of bot review comments — total / resolved / unresolved per repo + an `all_resolved` flag. Bot vs human classification respects `[augments] review_bots` in canopy.toml. |
| `canopy historian show [<feature>]` | **M4.** Print the rendered memory file for a feature (3 sections: resolutions log, PR context, sessions). Returns empty when no memory has been recorded yet. |
| `canopy historian compact [<feature>] [--keep-sessions <n>]` | **M4.** Trim the Sessions section to the most-recent N (default 5). Resolutions log + PR context are preserved regardless. v1 is mechanical (no LLM); future iterations will summarize. |
Expand Down Expand Up @@ -150,6 +153,15 @@ Install-staleness (canopy's installation around the workspace):

## Common patterns

Session start — returning to a feature:

```bash
canopy resume <feature> # switch-if-needed + fresh brief + bump last-visit anchor
# brief shows: window_hours, since_last_visit counts, current_state, intent_hints
canopy reply <thread_id> --body "Done — fixed in abc123." --resolve # close a thread
canopy resolve <thread_id> # close a thread without replying
```

The daily loop:

```bash
Expand Down
Loading
Loading