Plan 2: feature_resume brief + GH thread mutations (3.1.0)#24
Merged
Conversation
…ead_id on comments
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
canopy resume <alias>/mcp__canopy__feature_resume(alias)resolves the alias → switches the canonical slot if needed → refreshes GitHub + Linear → returns a structured brief withintent_hintsfor the most likely next actions.integrations/github.pyplus first-classcanopy resolve <thread_id>/canopy reply <thread_id> [--resolve]verbs and MCP tools.commit --addressoptionally takes the round-trip all the way through with--resolve-thread.switchnow bumpslast_visiton every successful switch and embeds a counts-onlysince_last_visit_summaryin 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 briefcanopy resolve <thread_id> [--feature] [--json]— close a GH thread + log locallycanopy 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 threadMCP
feature_resume(alias)— full briefresolve_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 attributeby_canopy: trueonthreads_resolved_on_github)Augment
[augments] auto_resolve_threads_on_address = true— makecommit --address --resolve-threadthe defaultModules added
actions/last_visit.py,actions/resume.py,actions/thread_actions.py,actions/thread_resolutions.pyExtended
integrations/github.py— GraphQL:list_review_threads,resolve_thread,unresolve_thread,reply_to_thread. Every comment fromget_review_commentsnow carriesthread_idandauthor_type(derived from GraphQL__typename).actions/switch.py—mark_visitedafterwrite_state;since_last_visit_summaryembedded in return (uses prior anchor, set todegraded: trueon GH failure).actions/commit.py—--resolve-threadhooks into the new GH GraphQL + writes attribution tothread_resolutions.json.using-canopyandaugment-canopyskills, plusCLAUDE.md,docs/concepts.md,docs/commands.md,docs/mcp.md,docs/agents.md,CHANGELOG.md.Plan + protocol
/Users/ashmit/.claude/plans/2026-05-28-feature-resume-brief.md(18 tasks across foundation + compound action).c2ad0c3,dc78643,1dbfe94).Test plan
pytest tests/— baseline 754 on main)tests/test_switch.py,tests/test_commit.py,tests/test_github.pypaths)canopy resume --help,canopy resolve --help,canopy reply --helpall show the new subparserscanopy resume DOC-xxxxagainst the docsum workspace, verify the brief carries actual PR/thread datacanopy switch X → switch Y → resume Y(canonical) advances last_visit once per resume; theprevious_visitfield reflects the prior anchorthread_resolutions.jsonrecordsvia_command: "commit_address"+via_commit_shaauto_resolve_threads_on_address = true, verifycommit --address <id>auto-resolves without the flag