feat: proactive auto-slot offer + collision guard (#241)#375
Merged
Conversation
Slotting writes meta["github"]["issues"], and write_file is a blind overwrite —
two sessions (or a teammate's pull) racing the same track silently lose one
writer's add. Add lib/membership_guard.py:
- issues_fingerprint(meta): deterministic sha256[:16] of the sorted issues list
ONLY, so unrelated concurrent edits (refresh-md stamping last_touched, handoff
rewriting the body) don't trip a false abort.
- guarded_membership_write(...): re-reads the file immediately before writing,
merges the add/remove delta onto the FRESH frontmatter, and writes back the
fresh body unchanged (preserves a concurrent body-only edit). With expect=<fp>
it aborts on a membership change since the caller's snapshot, returning
{stale, current, ...} instead of clobbering; expect=None never aborts.
slot/batch-slot route every write through the guard and accept --expect=<fp>.
The re-read+merge is unconditional (strictly safer; identical observable result
for a lone writer); only the abort is --expect-gated, so manual slot is
unchanged. Gate order is confirm-first (public-repo) then expect-last (staleness,
at write time) to minimize the TOCTOU window. In --expect mode advisory notes go
to stderr so stdout stays pure JSON for the {stale} signal the viewer parses.
Tests: guard unit tests (real temp-file round-trip) incl. body preservation +
stale abort; slot --expect match/mismatch/confirm-then-stale ordering. Existing
slot/batch-slot/move drivers updated to mock the guard's parse_file/write_file.
1134 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fingerprint CAS (phase 1) closes the same-machine race. Shared-tier tracks
also travel via git push/pull, so a teammate's pushed plan change is a
cross-machine race the CAS alone can't see. Add a rebase guard:
- plan_worktree.rebase_onto_origin(worktree, branch): fetch origin/<branch> then
rebase the worktree onto it. Returns True when at-or-ahead (rebased, nothing to
replay, or branch unpublished); False on conflict — which it ABORTS first, so
the worktree is never left half-rebased. Never raises.
- membership_guard.shared_rebase_guard(target, cfg): for a shared track pinned to
a plan_branch, resolve its worktree and rebase before writing. No-op for
private tracks, legacy shared tracks (no plan_branch), or an unresolvable
worktree (degrade). Returns (False, reason) only on un-rebasable divergence.
slot/batch-slot run the guard right after the public-repo confirm gate: on
divergence they emit {needs_rebase, reason, track} and return without writing,
so the viewer re-prompts instead of clobbering a diverged shared branch. A clean
rebase pulls the teammate's change in; the phase-1 CAS then trips on the now-
stale offer and the viewer re-offers against merged state.
Tests: rebase clean/unpublished/conflict-abort/git-unavailable; guard
private/legacy/clean/divergence/worktree-unavailable; slot {needs_rebase} abort
writes nothing. 1144 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 2)
From the /security-review pass on the shared-git path:
- MAJOR (1.1): rebase_onto_origin now uses `git rebase --autostash`. The normal
flow writes the file then commits, so the worktree's .work-plan/ is routinely
dirty at rebase time; plain rebase refused the dirty precondition and reported
a spurious {needs_rebase} with a misleading "diverged" reason. Autostash
shelves + reapplies local plan edits so a clean rebase succeeds.
- MINOR (5.1, surfaced pre-existing): the advisory `gh issue view` milestone
check sits between the rebase guard and the write and was un-try/excepted — a
missing gh (FileNotFoundError) or non-JSON stdout would crash the command
after a successful rebase. Wrapped in slot + batch-slot; the write still
completes. Regression test added.
- Docs (2.1/2.2/3): reworded the membership_guard docstrings to stop
overselling atomicity — it's a check-then-act (not a locked CAS), the shared
rebase is best-effort-onto-origin-as-of-last-fetch (cross-machine atomicity
relies on non-ff push rejection + rebase-on-next-write), and the body/other
frontmatter fields are preserved (only the issues list is replaced) modulo
yq re-serialization.
Accepted as-is (non-blocking): blanket except in shared_rebase_guard (fail-open
is correct for collaborative planning state), worktree cache GC (pre-existing
nit). 1145 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
phase 3) Prepares auto-triage to drive the viewer's Suggested bucket: - --json scan mode emits one JSON object on stdout (batch_id + untracked + tracks + prompt + answers path) so the extension captures the batch_id synchronously. Progress moved to stderr to keep stdout pure JSON. - Per-repo cache files (auto_triage.<repo-slug>.json/.answers.json) so two repos don't clobber the single fixed path; legacy fixed names kept when no --repo (back-compat with the terminal flow + existing tests). - batch_id stamped in the batch and echoed by the answers; --apply warns on a mismatch (a stale older-scan answers file) but still applies. - v2 abstain-first answers schema sniffed in --apply: {version,batch_id, suggestions:[{issue,verdict,track,runner_up,confidence,margin,rationale}]}. Only verdict==suggest with non-narrow margin is slotted; abstains/narrow stay untracked. Legacy v1 [{track,issues}] still applies unchanged. Model-authored fields hardened (int-coerce, skip malformed). - Prompt re-pitched for precision (per-issue, abstain-the-default, grounded rationale, top+runner-up) and the batch now carries each track's scope text so matching isn't just slug-string pattern-matching (ai-engineer review). - SKILL.md: write answers atomically (.tmp+rename) since the viewer watches the file live; use v2 + copy the batch_id; prefer abstain. Tests: --json emits batch_id+prompt+scope; v2 applies only clear suggestions (abstain/narrow left untracked); v1 still applies; batch_id mismatch warns. 1149 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rite (phase 4a)
Wires the CLI's compare-and-swap + shared-rebase guards (phases 1-2) to the
viewer's write path:
- slot/batchSlot WriteActions take an optional `expect` fingerprint; actionToArgs
emits it as --expect=<fp> before the `--` separator (omitted → unchanged).
- WriteOutcome gains {status:"stale", current} and {status:"needsRebase"}.
- executeWrite detects the CLI's {stale}/{needs_rebase} JSON on BOTH the first
call and the confirmed re-invocation (a private slot never hits the confirm
gate; a public one carries both --confirm and --expect and can still come back
stale), via a new guardOutcome() helper. Callers re-offer on fresh state
instead of treating a declined write as success.
Tests: --expect emitted before '--' (slot + batchSlot); back-compat without
expect; first-call stale; needs_rebase; confirm-then-stale carrying both flags;
normal write not falsely tripped. 686 pass, tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
phase 4b) The visible viewer layer on the #241 CLI foundation. Opt-in via workPlan.autoSlotSuggestions (default off). - Suggest Tracks (on the Untracked bucket / a repo) runs `auto-triage --json`, records the batch_id, relays the AI prompt to the output channel, and fs.watches the CLI-emitted answers_path (verbatim; 300ms debounce, cold-open read, disposed on deactivate). - A "Suggested" sub-bucket (clear margin + confidence >= threshold → one-click Accept) and a "Needs review" sub-bucket (narrow/low-confidence → open-only) nest as the first children of Untracked. Bucketed issues are removed from the plain untracked list so each shows once. Rationale-led labels; confidence in the tooltip (never color-only). sparkle/lightbulb icons, count badges. - Accept computes the CAS fingerprint of the target track's live issue list and slots via executeWrite({kind:"slot"|"batchSlot", expect}); branches on written / cancelled / stale (re-offer) / needsRebase (warn). Reuses confirmPublicWrite, refreshAfterWrite, withWriteProgress, the candidate-track QuickPick. Accept All groups by track; Dismiss / Dismiss All persist to workspaceState (autoSlot.dismissed.<repo>.<n>). Threshold change re-buckets. - New: src/fingerprint.ts (byte-matches the Python issues_fingerprint — cross-language vector verified) + src/suggestions.ts (tolerant v2 parse, batch_id validation, three-tier bucketing, abstain/dismiss exclusion). - package.json: 5 commands (palette-reachable + node menus), 2 config props. - vscode/README.md feature bullet (## Status left for the deploy bump). Built by a delegated implementer, reviewed here: fixed a plain-list/sub-bucket duplication; independently verified the fingerprint cross-language match, tsc clean, and 711 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #241 Suggested bucket only populated when a Claude session wrote the answers. This adds a no-LLM fallback so it works standalone. CLI: - lib/heuristic_triage.py: pure, deterministic scorer. Per untracked issue, scores each candidate track on local signals only (milestone match, track-label overlap incl. the track/<slug> default, title/scope keyword overlap), abstain-first (a track must clear a min score), margin clear/narrow from the top-vs-runner gap, grounded rationale naming the matched signal. - `auto-triage --heuristic` runs the scorer and writes the v2 answers file itself (atomic .tmp+replace, stamped source:"heuristic", same batch_id as the scan), so --apply and the viewer consume it unchanged. Combines with --json. Viewer: - autoTriageScan({heuristic}); a "Suggest Tracks (offline heuristic)" command (shared scan driver with the LLM variant). The CLI writes the answers during the scan, so the watcher's cold read populates the bucket immediately — no session. suggestions.ts threads `source`; the Suggested group shows "· heuristic" + a lower-trust tooltip when source==heuristic. Tests: scorer (milestone/label/keyword/abstain/margin/clamp/malformed); --heuristic writes the answers file with source + correlating batch_id; source threading in parseSuggestions. CLI 1159, VS Code 713, tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
Update: also includes #373 — offline heuristic suggestion mode, so the Suggested bucket works with no Claude session. |
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 #241 (when this reaches
main).Proactively offer to slot untracked GitHub issues into existing tracks, with an AI-suggested destination per issue — plus the hard collision-prevention the issue requires. Designed across six agents (exploration, ux-designer, backend-architect, ai-engineer) and built in four phases; the shared-git path got a dedicated
/security-review.Phases (each a commit; CLI phases 1–3 are independently valuable)
1 — CAS collision guard (
lib/membership_guard.py).issues_fingerprint(sha256[:16] of the sorted issue list only — unrelated frontmatter/body edits don't trip it).guarded_membership_writere-reads the file immediately before writing, merges the delta onto the fresh frontmatter, and writes back the fresh body (preserves concurrent body/other-field edits); with--expect=<fp>it aborts{stale}on a membership change instead of clobbering. Wired intoslot/batch-slot; re-read+merge is unconditional, abort is--expect-gated, so manual slotting is unchanged. Confirm-gate first, staleness CAS last.2 — shared-tier rebase guard (
plan_worktree.rebase_onto_origin). For a shared track on aplan_branch, fetch +rebase --autostashonto origin before writing; an un-rebasable divergence aborts{needs_rebase}. Security-review fixes folded in:--autostash(the write-then-commit flow leaves the worktree routinely dirty), a wrap around the advisoryghmilestone call (couldn't crash between rebase and write), and honest docstrings (check-then-act, not a locked CAS; best-effort onto-origin, not cross-machine atomic).3 — suggestion engine (
auto-triage).--jsonscan emits the batch +batch_id+ prompt + answers path on stdout; per-repo cache files; a v2 abstain-first answers schema ({version,batch_id,suggestions:[{issue,verdict,track,confidence,margin,rationale}]}) sniffed in--apply(only clear-margin suggests slotted; legacy v1 still works); prompt re-pitched for precision with track scope text. SKILL.md: write answers atomically, prefer abstain.4 — viewer (opt-in
workPlan.autoSlotSuggestions). A Suggested sub-bucket (clear margin + confidence ≥ threshold → one-click Accept) and a Needs review sub-bucket (open-only) under Untracked; bucketed issues leave the plain list. Accept computes the CAS fingerprint of the live target track (TSissuesFingerprintbyte-matches the Python one — cross-language vector verified), slots viaexecuteWritewith--expect, and branches on written/cancelled/stale(re-offer)/needsRebase(warn). Suggest Tracks runs the--jsonscan +fs.watches the answers path; Dismiss/Dismiss-All persist toworkspaceState. 5 palette-reachable commands, 2 config keys.Verification
tsc --noEmitclean.[10,20]→28c5e638bdf2b3cdin both)./security-reviewon the shared-git path; findings fixed (above).Notes
vscode/package.jsonversion +vscode/README.md## Statusto be hand-bumped at deploy time per the release flow.🤖 Generated with Claude Code