Skip to content

Plan 2: feature_resume brief + GH thread mutations (3.1.0)#24

Merged
ashmitb95 merged 21 commits into
mainfrom
plan-2-feature-resume
May 30, 2026
Merged

Plan 2: feature_resume brief + GH thread mutations (3.1.0)#24
ashmitb95 merged 21 commits into
mainfrom
plan-2-feature-resume

Conversation

@ashmitb95

Copy link
Copy Markdown
Owner

Summary

  • One-shot session-start primitive: canopy resume <alias> / mcp__canopy__feature_resume(alias) resolves the alias → switches the canonical slot if needed → refreshes GitHub + Linear → returns a structured brief with intent_hints for the most likely next actions.
  • Canopy can finally close + reply to GH review threads: new GraphQL helpers in integrations/github.py plus first-class canopy resolve <thread_id> / canopy reply <thread_id> [--resolve] verbs and MCP tools. commit --address optionally takes the round-trip all the way through with --resolve-thread.
  • switch now bumps last_visit on every successful switch and embeds a counts-only since_last_visit_summary in its return — the agent sees "something changed" without a full resume round-trip.

Bumps canopy to 3.1.0 (Plan 1 shipped 3.0.0). Adds 13 modules / extends 8 / adds 67 MCP tools total.

What changed

CLI

  • canopy resume <alias> [--json] [--reset-anchor] — the full brief
  • canopy resolve <thread_id> [--feature] [--json] — close a GH thread + log locally
  • canopy reply <thread_id> [--body | --body-file | stdin] [--resolve] [--feature] [--json]
  • canopy commit --address <id> [--resolve-thread | --no-resolve-thread] — round-trip the addressing commit through the thread

MCP

  • feature_resume(alias) — full brief
  • resolve_thread(thread_id, feature?)
  • reply_to_thread(thread_id, body, feature?, resolve_after?)

State files (new, atomic temp+rename)

  • .canopy/state/visits.json{feature: {last_visit, previous_visit}}
  • .canopy/state/thread_resolutions.json{thread_id: {resolved_by_canopy_at, feature, via_command, via_commit_sha}} (used so the brief can attribute by_canopy: true on threads_resolved_on_github)

Augment

  • [augments] auto_resolve_threads_on_address = true — make commit --address --resolve-thread the default

Modules added

  • actions/last_visit.py, actions/resume.py, actions/thread_actions.py, actions/thread_resolutions.py

Extended

  • integrations/github.py — GraphQL: list_review_threads, resolve_thread, unresolve_thread, reply_to_thread. Every comment from get_review_comments now carries thread_id and author_type (derived from GraphQL __typename).
  • actions/switch.pymark_visited after write_state; since_last_visit_summary embedded in return (uses prior anchor, set to degraded: true on GH failure).
  • actions/commit.py--resolve-thread hooks into the new GH GraphQL + writes attribution to thread_resolutions.json.
  • Bundled using-canopy and augment-canopy skills, plus CLAUDE.md, docs/concepts.md, docs/commands.md, docs/mcp.md, docs/agents.md, CHANGELOG.md.

Plan + protocol

  • Plan file: /Users/ashmit/.claude/plans/2026-05-28-feature-resume-brief.md (18 tasks across foundation + compound action).
  • Executed subagent-driven, one implementer per task; three milestone code reviews with fix commits along the way (c2ad0c3, dc78643, 1dbfe94).
  • Depends on Plan 1's slot model (3.0.0) — slot model must ship before this.

Test plan

  • 857 tests passing locally (pytest tests/ — baseline 754 on main)
  • No regressions in existing suites (tests/test_switch.py, tests/test_commit.py, tests/test_github.py paths)
  • CLI smokes: canopy resume --help, canopy resolve --help, canopy reply --help all show the new subparsers
  • Real GH integration smoke — run canopy resume DOC-xxxx against the docsum workspace, verify the brief carries actual PR/thread data
  • Switch round-tripcanopy switch X → switch Y → resume Y (canonical) advances last_visit once per resume; the previous_visit field reflects the prior anchor
  • commit --address --resolve-thread — pick a bot comment, run it end-to-end, verify the GH thread closes + thread_resolutions.json records via_command: "commit_address" + via_commit_sha
  • Augment default — set auto_resolve_threads_on_address = true, verify commit --address <id> auto-resolves without the flag

ashmitb95 added 21 commits May 30, 2026 07:29
Adds actions/thread_resolutions.py (atomic JSON log), actions/thread_actions.py
(resolve_thread + reply_to_thread wrappers calling GH + recording locally),
`canopy resolve <thread-id>` CLI subcommand, and mcp__canopy__resolve_thread
MCP tool. 12 new tests, full suite 770 passed.
… capture + gh -f for strings

MUST #1: Add author { __typename } to list_review_threads GraphQL query so
_build_comments_from_threads sets author_type="Bot"/"User" correctly.
Without this, _is_bot_comment never matched and bot detection was broken.

MUST #2 (scenario b): _commit_one only commits, does not push. The thread
resolve fires on local commit success, before the commit is on GitHub.
Added a code comment at the resolve guard to document this — no new push
step added; canopy separates commit and push by design.

MUST #3: reply_to_thread with resolve_after=True now catches ActionError
from the inner resolve_thread call and returns {"error": e.to_dict()}
instead of propagating — so the caller always sees posted + resolved.

SHOULD #4: _graphql_via_gh_cli now uses -f for string vars (body, id) and
-F only for int/bool vars, preventing String! args from being coerced to
the wrong GraphQL type.

SHOULD #5: resolved_count in _build_comments_from_threads now increments by
1 per resolved thread (not per comment), matching _normalize_comments.

SHOULD #7: cmd_reply_thread guards sys.stdin.read() with isatty() check and
exits with missing_reply_body BlockerError rather than blocking on stdin.

Cleanup: removed dead `from . import augments as augments_mod` in commit.py.

New tests: test_author_type_from_graphql_typename, test_resolved_count_counts_threads_not_comments,
test_get_review_comments_fallback_path_carries_thread_id (test_thread_graphql.py),
test_reply_to_thread_resolve_after_failure_keeps_posted (test_thread_actions.py).
780 tests, 0 failures.
Add log_since() to git/repo.py for ISO-timestamp-based commit filtering.
Implement _commits_since() and _populate_since() in actions/resume.py to
populate per-repo commits authored after the last-visit anchor.

Each repo in the feature maps to a list of {sha, short_sha, at, author, subject}.
Per-repo git errors default to empty list; no crash on missing branch/repo.

Add 3 tests:
- Happy path: commit made after anchor appears in resume brief
- First visit: commits not populated (no prior anchor)
- No-new-commits: all repos map to empty lists when no commits made after anchor

802 tests passing (799 + 3 new).
Populates three since_last_visit sub-sections:
- threads_new: unresolved threads whose first comment arrived after last_visit
- threads_resolved_on_github: resolved threads since last_visit with by_canopy flag
  (joined against thread_resolutions.json)
- threads_resolved_by_canopy: bot_resolutions entries for this feature since anchor

All GH calls are swallowed on error; brief always completes. 5 new tests added.
Implements T9: _populate_current now fills feature_state (or "unknown"
on failure), ci_summary_per_repo (lifted from summary.ci_per_repo),
and branch_position_per_repo (ahead/behind/last_sync_at per repo vs
default branch). The align_with_default intent hint fires correctly
when any repo has behind > 0. Adds 4 tests; suite at 811 passed.
Populate current_state.draft_replies_summary from draft_replies() call,
capturing addressed_total and unaddressed_total. Populate
since_last_visit.draft_replies_pending with count of addressed-but-not-yet-posted
drafts only when there is a prior anchor (not on first visit).

Both populate with graceful error handling — failures leave defaults in place.
The existing post_drafts intent hint already fires when addressed_total > 0,
now backed by real data.

Add 4 tests:
- draft_replies_summary populated from call
- post_drafts hint fires when addressed_total > 0
- draft_replies_pending only when anchor exists (0 on first visit)
- error handling preserves defaults

All 817 tests passing.
…coverage

- Populate current_state.linear_issue/linear_url from FeatureLane via
  FeatureCoordinator.status(); empty string normalized to None; lane
  lookup failures swallowed and default to None.
- Add _open_thread_count helper that rolls up unresolved threads per-PR
  using _pr_coords_per_repo + list_review_threads; TODO comment marks
  the 2x round-trip as a milestone-3 cache opportunity.
- Tests: investigate_ci hint, read_issue hint (first visit + linear_issue),
  read_issue absent on second visit, _pr_coords_per_repo direct (file://
  remotes → None), future-anchor edge case (2099 timestamp → empty
  since sections, no crash), open_thread_count rollup + zero paths,
  linear populated/none/failure-swallowed.
Add format_for_agent_since(workspace_root, feature, since_iso) to historian.py
to render only entries with timestamp > since_iso. This enables windowed session
summaries in the feature-resume brief.

Wire the new helper into _populate_since to populate since_last_visit[
historian_excerpt] when an anchor exists. Timestamps filtered lexicographically
using ISO 8601 Z format (e.g. "2026-05-26T15:30:00Z").

Tests added:
- historian: 5 tests for format_for_agent_since (filter, empty cases, headers, boundaries)
- resume: 4 tests for historian_excerpt integration (anchor, first visit, errors, filtering)

All 837 tests pass (9 new tests added).
T13: Call lv.mark_visited(workspace, feature_name) in _post_switch_persist
after slots_mod.write_state completes. Every successful switch into a feature
counts as a conscious look per the single-bump invariant.

This ensures the resume/switch separation of concerns: switch bumps once
after all state is committed, and resume does not bump again when switch ran.

Test coverage:
- test_switch_bumps_last_visit: basic bump on first switch
- test_switch_bumps_previous_visit_on_reswitch: previous_visit carries old anchor
- test_resume_single_bump_when_real_switch_runs: single-bump invariant with real switch

Acceptance criteria met:
- Every successful switch bumps last_visit
- No bump on failed switch (mark_visited after write_state)
- Single-bump invariant holds: resume skips bump when switch ran
- All tests passing (57 in test_switch/resume, full suite pending)
Add "Session start — call feature_resume first" and "Closing out review
threads" sections to the using-canopy skill, positioning feature_resume
as the compound session-start primitive that supersedes switch alone.
…unresolved tests

- docs/concepts.md: replace stale brief example with actual feature_resume shape;
  fix next_actions→intent_hints; fix switch/resume bump contradiction; drop
  false debounce claim; document single-bump invariant correctly
- docs/mcp.md: drop next_actions from feature_resume description; fix
  resolved_threads→threads_resolved_by_canopy in resolve_thread row
- docs/agents.md: remove "(also in resume brief)" parenthetical for next_actions
- augment-canopy SKILL.md (bundled + user): add auto_resolve_threads_on_address row
- tests/test_resume.py: add multi-repo threads_new aggregation test and
  unresolved-with-stale-resolved_at exclusion test
@ashmitb95 ashmitb95 merged commit 5d0e6d9 into main May 30, 2026
4 checks passed
@ashmitb95 ashmitb95 deleted the plan-2-feature-resume branch May 30, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant