Skip to content

feat: proactive auto-slot offer + collision guard, offline heuristic, webview-handler fix#376

Merged
evemcgivern merged 3 commits into
mainfrom
dev
Jun 18, 2026
Merged

feat: proactive auto-slot offer + collision guard, offline heuristic, webview-handler fix#376
evemcgivern merged 3 commits into
mainfrom
dev

Conversation

@evemcgivern

Copy link
Copy Markdown
Contributor

Deploy of the auto-slot feature set plus a critical webview regression fix. Three issues.

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

Offer to slot untracked GitHub issues into existing tracks, with an AI-suggested destination per issue, and the hard collision-prevention the issue requires. Built in 4 phases:

  • CAS collision guardslot/batch-slot take --expect=<fp> (sha256 of the track's issue list); the write re-reads + merges onto fresh frontmatter and aborts {stale} instead of clobbering. Preserves concurrent body/other-field edits.
  • Shared-tier rebase guard — for a track on a plan_branch, fetch + rebase --autostash onto origin before writing; {needs_rebase} abort on un-rebasable divergence. /security-review'd.
  • Suggestion engineauto-triage --json scan + batch_id + a v2 abstain-first answers schema (legacy v1 still applies).
  • Viewer (opt-in workPlan.autoSlotSuggestions) — Suggested (one-click Accept) / Needs-review sub-buckets under Untracked; Accept slots through the guard + public-repo modal, branching on stale/needsRebase. 5 commands, fs.watch, dismiss state.

feat: offline heuristic suggestion mode (#373)

auto-triage --heuristic scores untracked issues against candidate tracks on local signals (milestone / track-label / title-scope keyword overlap), abstain-first, and writes the v2 answers file itself (source: "heuristic") — so the Suggested bucket works with no Claude session (lower-trust, offline). New viewer command Suggest Tracks (offline heuristic); heuristic suggestions are flagged lower-trust.

fix(vscode): webview handlers dead since 0.9.0 (#374)

Escaped quotes in an inline-script template literal collapsed to a SyntaxError, killing the entire messaging IIFE — every webview click handler (track select, focus toggle, the 0.12.0 graph zoom/pan/export controls) was silently dead from 0.9.0 onward. Fixed; added a parse-guard test that new Function()-checks every inline script so this class fails CI.

Ships

  • VS Code extension 0.14.0 (Marketplace + Open VSX) — 0.13.0 already shipped 2026-06-16.
  • npm @stylusnexus/work-plan (CLI: the guard, auto-triage --json/--heuristic, v2 answers).
  • CLI floor unchanged (2026.06.15).

Tests: CLI 1159 · VS Code 715 · tsc clean. Cross-language CAS fingerprint verified.

evemcgivern and others added 3 commits June 18, 2026 09:40
…webview handlers (#374)

The webview's inline <script> bodies are built as backtick template literals in
html.ts. The dep-toggle querySelector used `\"`, which the template literal
collapses to a bare `"`, emitting `data-depissue="" + depIssue + ""` — adjacent
string literals, a hard SyntaxError. That parse error killed the ENTIRE messaging
IIFE, so every webview click handler (track select, focus toggle, and the new
#216 graph zoom/pan/export controls) was silently dead. Shipped in #257 (0.9.0);
surfaced now via the new graph controls.

Fix: single-quote the selector string so no escaping is needed. Add a regression
guard that extracts every inline <script> and new Function()-parses it — the old
tests asserted on the HTML string but never executed the JS. Verified the guard
fails on the buggy line and passes on the fix; reproduced dead/working controls
in a real browser harness. 681 tests pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(slot): compare-and-swap membership guard (#241 phase 1)

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>

* feat(slot): shared-tier rebase guard before write (#241 phase 2)

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>

* fix(slot): security-review fixes for the shared-tier guard (#241 phase 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>

* feat(auto-triage): --json scan, batch_id, v2 abstain-first answers (#241 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>

* feat(vscode): thread the #241 staleness/rebase guard through executeWrite (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>

* feat(vscode): auto-slot Suggested bucket + accept/dismiss commands (#241 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>

* docs: README rows for slot --expect + auto-triage --json/v2 (#241)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(auto-triage): offline heuristic suggestion mode (#373)

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>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
0.13.0 already shipped (2026-06-16), so bump to 0.14.0 — republishing 0.13.0
would no-op on the Marketplace (--skip-duplicate). Prepend the 0.14.0 Status
entry: proactive auto-slot suggestions (#241) + offline heuristic (#373) + the
critical webview-handler fix (#374). CLI floor unchanged (2026.06.15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@evemcgivern evemcgivern merged commit 64ae461 into main Jun 18, 2026
18 checks passed
evemcgivern added a commit that referenced this pull request Jun 18, 2026
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