Repository navigation
feat(iterations): dryRun, disk-backed snapshots, per-iteration restore counts - #34
Merged
Merged
Conversation
The .claude/ scaffold here dated from 7 Apr and predated several cli
releases. Re-ran `plantas init --force --seiva --cross-session` on
2.12.0, which:
- migrated sessions.json to the JSON-aware plantas-sessions merge
driver (plain union concatenates single-line JSON into invalid
two-line JSON on a concurrent merge)
- dropped the context.md merge=union rule, which asserted the file was
tracked when it is gitignored
- installed the cross-session hooks and /plantas:{tail,status,search}
commands, added in cli 2.8.0 (2026-05-27)
context.md is untracked here too. The .gitignore entry alone did
nothing while the path stayed tracked, so every commit re-dirtied it
and it showed up as PR churn. File stays on disk; the post-commit hook
regenerates it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e counts Closes #32, closes #33. The iteration tools have no per-iteration mutation available to them: `updateProjectV2Field` replaces the whole configuration, regenerating every iteration id and detaching every item's Sprint value, so both tools snapshot the assignments and restore them afterwards. Three gaps in that mechanism: **The snapshot was memory-only (#32).** The restore is one mutation per assigned item, so on a 150-assignment board the window between "every value detached" and "every value restored" is minutes long. A death inside it — dropped connection, rate limit, a stopped MCP client — lost every assignment with nothing to retry against, since the destructive mutation had already succeeded. The snapshot is now written to ~/.config/github-projects-mcp/iteration-snapshots/ before the mutation and its path returned as `snapshotPath`; `restore_iteration_snapshot` replays it against the field's current ids. A write failure aborts the call before it mutates, because proceeding would enter exactly the unrecoverable window the file exists to close. **Nothing could be inspected before running (#33).** Both tools take `dryRun`, returning the iterations before and after, the assignments that would be snapshotted broken down per iteration, and any detected rename with the number of entries it would remap — that last one makes the rename remap visible from outside for the first time. Everything it reports is already fetched before the mutation, so a dry run is an early return, not extra queries. **`assignmentsRestored` reported counts without destinations (#33).** It now carries `byIteration` alongside `restored`/`failed`, so a caller can diff the dry run's breakdown against the result's instead of re-reading the whole board to verify. **Duplicate titles silently misassigned items.** Restore is keyed by title, the only identifier surviving the mutation, and the lookup is a Map — two iterations sharing a title collapsed to one entry and every affected item landed on whichever id came last, with the call reporting success and no failures. Both tools now refuse a payload that would leave duplicate titles, naming the offender. This turns a silently wrong outcome into a rejected call, so it is a behaviour change for any board that already holds duplicates. Also fixes the MCP server's advertised version, which was pinned at 1.5.0 while package.json had moved to 1.6.1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #32, closes #33.
Both issues are about the same mechanism.
updateProjectV2Fieldis the only route to add or edit a single iteration, and it replaces the whole configuration — regenerating every iteration id and detaching every item's Sprint value. Both tools already snapshot the assignments and restore them afterwards; this makes that mechanism survivable, inspectable, and verifiable.#32 — the snapshot lived only in memory
The restore is one mutation per assigned item, so on a 150-assignment board the window between every value detached and every value restored is minutes long. A death inside it lost every assignment with nothing to retry against, since the destructive mutation had already succeeded.
~/.config/github-projects-mcp/iteration-snapshots/<projectId>-<timestamp>.jsonbefore the mutation, and its path returned assnapshotPath. Location followssrc/auth/token-store.tsrather than the~/.plantas/the issue suggested, so all local state stays in one place.restore_iteration_snapshottool replays a snapshot against the field's current iteration ids. Idempotent.#33 — nothing could be inspected before running
dryRun: trueon both tools returns:{ "dryRun": true, "operation": "update_iteration", "iterationsBefore": [ /* current, ids included */ ], "iterationsAfter": [ /* no ids — every one is regenerated */ ], "rename": { "from": "Phase 7: A", "to": "Phase 7: B", "assignmentsRemapped": 1 }, "assignments": { "total": 24, "byIteration": { "Phase 7: B": 1, "Phase 6": 23 } } }Everything it reports is already fetched before the mutation, so a dry run is an early return rather than extra queries — two read calls, no mutation, no snapshot file. The
renamefield makes the remap atiterations.ts:631visible from outside for the first time.assignmentsRestorednow carriesbyIterationalongsiderestored/failed, so the dry run's breakdown and the real result can be diffed instead of re-reading the whole board.Also: duplicate titles silently misassigned items
Not in either issue; found while reading the restore path and commented on #33.
Restore is keyed by title — the only identifier surviving the mutation — and the lookup is a
Map, so two iterations sharing a title collapsed into one entry and every affected item landed on whichever id came last. The call reported success withfailed: []; the items were simply on the wrong sprint. Reachable byadd_iterationwith an existing title, or byupdate_iterationrenaming onto a sibling.Both tools now refuse a payload that would leave duplicate titles, naming the offender, on the config read alone — before the snapshot query or the mutation.
Important
This is a behaviour change. A board that already holds duplicate iteration titles will now get a rejected call where it previously got a silently-wrong one. Judged worth it — there is no correct outcome available in that state — but it is the one thing here that is not purely additive. Called out since the repo has no CHANGELOG.
Version
1.6.1→1.7.0. Also fixes the MCP server's advertised version, which was pinned at1.5.0insrc/index.tswhilepackage.jsonhad moved to1.6.1.Tests
102 passing(was 64). New coverage: dry-run shapes and that they issue no mutation; per-iteration counts; rename detection and remap counts; duplicate-title rejection at the right point in the sequence; that the snapshot is written before the mutation (asserted on call ordering); that a failed write blocks the mutation; snapshot replay against fresh ids; and the store's own filename sanitising, versioning, and validation.iterations.test.tsmocks the snapshot store — the first run of these tests wrote six real files into~/.configbefore that was added.🤖 Generated with Claude Code