Skip to content

feat: proactive auto-slot offer + collision guard (#241)#375

Merged
evemcgivern merged 8 commits into
devfrom
feat/241-auto-slot
Jun 18, 2026
Merged

feat: proactive auto-slot offer + collision guard (#241)#375
evemcgivern merged 8 commits into
devfrom
feat/241-auto-slot

Conversation

@evemcgivern

Copy link
Copy Markdown
Contributor

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_write re-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 into slot/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 a plan_branch, fetch + rebase --autostash onto 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 advisory gh milestone 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). --json scan 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 (TS issuesFingerprint byte-matches the Python one — cross-language vector verified), slots via executeWrite with --expect, and branches on written/cancelled/stale(re-offer)/needsRebase(warn). Suggest Tracks runs the --json scan + fs.watches the answers path; Dismiss/Dismiss-All persist to workspaceState. 5 palette-reachable commands, 2 config keys.

Verification

  • CLI 1149 pass; VS Code 711 pass; tsc --noEmit clean.
  • Fingerprint cross-language match verified ([10,20]28c5e638bdf2b3cd in both).
  • /security-review on the shared-git path; findings fixed (above).

Notes

🤖 Generated with Claude Code

evemcgivern and others added 8 commits June 17, 2026 14:17
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>
@evemcgivern

Copy link
Copy Markdown
Contributor Author

Update: also includes #373 — offline heuristic suggestion mode, so the Suggested bucket works with no Claude session. auto-triage --heuristic scores untracked issues against candidate tracks on local signals (milestone / track-label / keyword overlap), abstain-first, and writes the v2 answers file itself (source: "heuristic"). New viewer command Suggest Tracks (offline heuristic); heuristic suggestions are flagged lower-trust in the tree. Closes #373 on deploy. CLI 1159 / VS Code 713 / tsc clean.

@evemcgivern evemcgivern merged commit 59b6582 into dev Jun 18, 2026
9 checks passed
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