@@ -15,27 +15,39 @@ proxy `/ws` to the WS server, so the UI uses the same same-origin
1515Use this when editing ` packages/server ` or ` packages/ui-browser ` and you
1616want changes to reload instantly instead of rebuilding/republishing.
1717
18- > ** Keep Step 1–3 in sync with ` .claude/skills/askdiff/SKILL.md ` .** The
19- > diff-resolution flow (interpret → git → temp file → label) must behave
20- > identically in both skills; only Step 4 (launch) differs. If you change
21- > the table or the bash blocks below, change them in the user-facing
18+ > ** Keep Steps 1–4 in sync with ` .claude/skills/askdiff/SKILL.md ` .** The
19+ > diff-resolution and session-resolution flow (interpret → git → temp file
20+ > → label → pick session) must behave identically in both skills; only
21+ > Step 5 (launch) differs. If you change any block below — including the
22+ > session-resolution logic in Step 4 — change it in the user-facing
2223> ` askdiff ` skill too.
2324
24- ## Step 1 — figure out which diff the user wants
25+ ## Step 1 — figure out which diff the user wants (and which session)
2526
2627Look at the message that invoked this skill. Anything after ` /askdiff-dev `
27- is the user's diff description ( may be empty).
28+ is free-form natural language that may carry ** two ** kinds of information:
2829
29- | User said | git command | Suggested label |
30+ 1 . A ** diff description** — what to diff (handled by the table/ladder
31+ below). This part is what Step 2 turns into a ` git diff ` command.
32+ 2 . An optional ** session hint** — which Claude session to attach to
33+ (handled by the * Session hint* subsection at the end of this step,
34+ then resolved in Step 4).
35+
36+ Either or both may be empty. The diff-description part may be empty
37+ (working tree); the session hint defaults to "the invoking session" when
38+ absent. Treat them independently — first identify and set aside the
39+ session hint, then pass the rest to the diff resolution below.
40+
41+ | ` diff_description ` | git command | Suggested label |
3042| ---| ---| ---|
31- | ` /askdiff-dev ` (no args ) | working tree — see Step 2 | ` Working tree ` |
32- | ` /askdiff-dev last commit` | ` git diff HEAD~1 HEAD ` | ` HEAD~1..HEAD ` |
33- | ` /askdiff-dev last 3 commits` | ` git diff HEAD~3 HEAD ` | ` HEAD~3..HEAD ` |
34- | ` /askdiff-dev the 5th latest commit` | ` git diff HEAD~5 HEAD~4 ` | ` HEAD~5..HEAD~4 ` |
35- | ` /askdiff-dev current branch against feature/test` | ` git diff feature/test...HEAD ` (three-dot, PR semantics) | ` feature/test…HEAD ` |
36- | ` /askdiff-dev main vs my branch` | ` git diff main...HEAD ` | ` main…HEAD ` |
37- | ` /askdiff-dev abc123 vs def456` | ` git diff abc123 def456 ` | ` abc123..def456 ` |
38- | ` /askdiff-dev staged` | ` git diff --cached ` | ` staged ` |
43+ | (empty ) | working tree — see Step 2 | ` Working tree ` |
44+ | ` last commit ` | ` git diff HEAD~1 HEAD ` | ` HEAD~1..HEAD ` |
45+ | ` last 3 commits ` | ` git diff HEAD~3 HEAD ` | ` HEAD~3..HEAD ` |
46+ | ` the 5th latest commit ` | ` git diff HEAD~5 HEAD~4 ` | ` HEAD~5..HEAD~4 ` |
47+ | ` current branch against feature/test ` | ` git diff feature/test...HEAD ` (three-dot, PR semantics) | ` feature/test…HEAD ` |
48+ | ` main vs my branch ` | ` git diff main...HEAD ` | ` main…HEAD ` |
49+ | ` abc123 vs def456 ` | ` git diff abc123 def456 ` | ` abc123..def456 ` |
50+ | ` staged ` | ` git diff --cached ` | ` staged ` |
3951
4052Defaults when the user is ambiguous:
4153- "branch X against branch Y" / "X vs Y" between two named refs ⇒ three-dot
@@ -104,6 +116,48 @@ each ref the user named directly. If any fails, stop and tell the user
104116which ref didn't resolve — do not launch the server. (Refs returned by the
105117search ladder are already validated by virtue of ` git log ` finding them.)
106118
119+ ### Session hint (optional)
120+
121+ By default ` /askdiff-dev ` attaches the WS server to the ** invoking**
122+ session (the one running this skill). The user may override that by
123+ carrying a phrase about the target session in their input. Decompose the
124+ input into two parts:
125+
126+ - ` diff_description ` — what to diff (everything Step 1's table/ladder uses)
127+ - ` session_hint ` — one of ` none ` , ` explicit-id <uuid-or-prefix> ` , or
128+ ` keywords <a, b, c, …> `
129+
130+ ** Trigger phrases** for ` session_hint ` (illustrative — generalize from
131+ these):
132+
133+ - "attached to (the/a) session …"
134+ - "connected to (the/a) session/conversation/chat …"
135+ - "in (the/a/our) session [ about / where / that] …"
136+ - "from (the/a) [ chat / conversation] [ where / about] …"
137+ - "the session in which …", "session that …"
138+ - "session id ` <uuid> ` ", "session ` <uuid> ` ", or a bare UUID-shaped token
139+ (8+ hex chars, optionally with dashes)
140+
141+ ** Examples:**
142+
143+ | User input | ` diff_description ` | ` session_hint ` |
144+ | ---| ---| ---|
145+ | ` last commit ` | ` last commit ` | none |
146+ | (empty) | (working tree) | none |
147+ | ` the staleness commit attached to the session where we discussed mtime checks ` | ` the staleness commit ` | keywords: "mtime checks" |
148+ | ` last commit in our session about pricing rules and tax math ` | ` last commit ` | keywords: "pricing rules", "tax math" |
149+ | ` session 322bc90a ` | (working tree) | explicit-id: ` 322bc90a ` |
150+ | ` abc123 vs def456 in session 322bc90a-714f-41b7-914e-109404e46072 ` | ` abc123 vs def456 ` | explicit-id: full UUID |
151+
152+ ** Be conservative.** If parsing is itself ambiguous (e.g. "the foo session"
153+ — is "session" a noun in the diff or a trigger?), treat the whole input
154+ as ` diff_description ` (no session hint). Don't ask the user to clarify the
155+ parse — just resolve the diff and proceed; the default attachment to the
156+ invoking session is always safe.
157+
158+ The session hint is consumed in Step 4. Steps 2 and 3 use only
159+ ` diff_description ` .
160+
107161## Step 2 — write the diff to a session-stable file
108162
109163First resolve the parent Claude Code session and project cwd. All ` /tmp `
@@ -160,15 +214,155 @@ show "No changes."
160214** Mark the diff as volatile if you took the working-tree path.** Set
161215` volatile=1 ` if Step 2 used the working-tree block (the diff can drift as
162216the user keeps editing); set ` volatile=0 ` for description-based diffs
163- (immutable git history). Step 4 forwards this to the server as
217+ (immutable git history). Step 5 forwards this to the server as
164218` ASKDIFF_DIFF_VOLATILE ` , which gates the per-file mtime staleness check.
165219
166220## Step 3 — pick a short label
167221
168222Use the "Suggested label" column above. For the working-tree case, use
169223` Working tree ` . Keep it under ~ 40 chars. This becomes ` ASKDIFF_DIFF_LABEL ` .
170224
171- ## Step 4 — launch (in-repo)
225+ ## Step 4 — resolve the target session
226+
227+ Compute ` attached_session ` and ` session_source ` from ` session_hint `
228+ (captured in Step 1). The default is the invoking session — that path
229+ matches today's behavior and skips all the matching logic below.
230+
231+ ``` bash
232+ attached_session=" $session_id " # default = invoking
233+ session_source=" invoking"
234+
235+ sessions_dir=" ${CLAUDE_CONFIG_DIR:- $HOME / .claude} /projects/$( echo " $project_cwd " | tr ' /' ' -' ) "
236+ ```
237+
238+ ### 4a. No hint → invoking session (default)
239+
240+ If ` session_hint ` is ` none ` , leave the defaults and skip to Step 5.
241+
242+ ### 4b. Explicit ID → resolve
243+
244+ If ` session_hint ` is ` explicit-id <X> ` :
245+
246+ ``` bash
247+ explicit_id=" <X>"
248+
249+ if echo " $explicit_id " | grep -qE ' ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' ; then
250+ # Full UUID: trust it if the file exists.
251+ if [ -f " $sessions_dir /$explicit_id .jsonl" ]; then
252+ attached_session=" $explicit_id "
253+ session_source=" explicit"
254+ else
255+ # → AskUserQuestion: session not found, use current?
256+ :
257+ fi
258+ else
259+ # Short prefix: glob and disambiguate.
260+ shopt -s nullglob
261+ matches=( " $sessions_dir /${explicit_id} " * .jsonl )
262+ shopt -u nullglob
263+ case ${# matches[@]} in
264+ 1)
265+ attached_session=$( basename " ${matches[0]} " .jsonl)
266+ session_source=" explicit"
267+ ;;
268+ 0)
269+ # → AskUserQuestion: no session matches "<prefix>", use current?
270+ : ;;
271+ * )
272+ # → AskUserQuestion: pick one of N candidates (list short-uuid · age)
273+ : ;;
274+ esac
275+ fi
276+ ```
277+
278+ For the AskUserQuestion branches above:
279+
280+ - ** Not found / 0 matches** : options are "Use current session" and "Cancel" (do not launch).
281+ - ** Multiple prefix matches** : one option per UUID labelled ` <short-uuid> · <age> ` (compute age below), plus "Use current session". Set ` attached_session ` and ` session_source="explicit" ` from the user's pick.
282+
283+ ### 4c. Keywords → grep, decide, possibly ask
284+
285+ If ` session_hint ` is ` keywords <a, b, c, …> ` :
286+
287+ ``` bash
288+ needles_file=$( mktemp)
289+
290+ # 1. The user's session keywords (literal phrases — one per line).
291+ printf ' %s\n' " <keyword 1>" " <keyword 2>" >> " $needles_file "
292+
293+ # 2. Changed file paths from the resolved diff (additional signal,
294+ # catches sessions that Read/Edit/Write'd those files).
295+ command grep -E ' ^\+\+\+ b/' " $diff_file " | sed -E ' s|^\+\+\+ b/||' >> " $needles_file "
296+
297+ # 3. Commit SHAs (only for description-based diffs — Claude knows these
298+ # from Step 1's resolution). Skip for working-tree diffs.
299+ for sha in " <sha1>" " <sha2>" ; do
300+ [ -n " $sha " ] && printf ' %s\n' " $sha " >> " $needles_file "
301+ done
302+
303+ # 4. Branch names (only for the X...Y / X..Y form).
304+ for br in " <branch1>" " <branch2>" ; do
305+ [ -n " $br " ] && printf ' %s\n' " $br " >> " $needles_file "
306+ done
307+
308+ # Search recent JSONLs (mtime −30d), filter out the invoking session
309+ # (it always matches its own JSONL because the user just typed the
310+ # keywords into it), return at most 5 rows of "<count> <uuid>" sorted
311+ # by hit count desc.
312+ #
313+ # Three subtle things below — change them at your peril:
314+ # - `command grep` bypasses any shell function/alias that wraps grep.
315+ # Claude Code's harness wraps grep as a function that proxies to
316+ # ugrep with extra flags, and that wrapper breaks `-Ff <patternfile>`.
317+ # - We pipe `find` directly into `while read`, instead of `for f in
318+ # $(find ...)`. zsh doesn't word-split unquoted variable expansions
319+ # on newlines by default; that for-loop iterates exactly ONCE with
320+ # $f containing every path concatenated.
321+ # - `count=$(grep -c ...)` then `[ -z "$count" ] && count=0` — do NOT
322+ # write `count=$(grep -c ... || echo 0)`. grep -c always prints a
323+ # number (0 on no match) AND exits non-zero when there are no
324+ # matches, so the `|| echo 0` doubles the output to "0\n0" and
325+ # breaks the numeric `-gt` comparison.
326+ results=$(
327+ find " $sessions_dir " -name ' *.jsonl' -mtime -30 -type f 2> /dev/null \
328+ | while read -r f; do
329+ uuid=$( basename " $f " .jsonl)
330+ [ " $uuid " = " $session_id " ] && continue
331+ count=$( command grep -cFf " $needles_file " " $f " 2> /dev/null)
332+ [ -z " $count " ] && count=0
333+ [ " $count " -gt 0 ] && echo " $count $uuid "
334+ done | sort -rn | head -5
335+ )
336+ rm -f " $needles_file "
337+ ```
338+
339+ Read ` $results ` and route:
340+
341+ | Result shape | Action |
342+ | ---| ---|
343+ | 0 lines | AskUserQuestion: "no session matched ` <keywords> ` . Use current session?" → "Use current" or "Cancel and refine" |
344+ | 1 line | use that UUID; ` attached_session=$uuid ` , ` session_source="matched" ` |
345+ | 2+ lines, top count ≥ 2× second | use top-1; ` session_source="matched" ` |
346+ | 2–5 lines, comparable counts | AskUserQuestion: list each candidate as ` <short-uuid> · <age> · <count> hits ` , plus "Use current session" |
347+
348+ For ages (used in AskUserQuestion labels):
349+
350+ ``` bash
351+ now=$( date +%s)
352+ mtime=$( stat -f %m " $sessions_dir /$uuid .jsonl" 2> /dev/null || stat -c %Y " $sessions_dir /$uuid .jsonl" )
353+ age_sec=$(( now - mtime ))
354+ if [ $age_sec -lt 86400 ]; then
355+ age_str=" $(( age_sec / 3600 )) h ago"
356+ else
357+ age_str=" $(( age_sec / 86400 )) d ago"
358+ fi
359+ ```
360+
361+ ** Don't widen the search automatically.** If results are empty or
362+ unclear, surface that to the user via AskUserQuestion. Re-run with
363+ broader scope (e.g. ` mtime -90 ` ) only if the user explicitly says to.
364+
365+ ## Step 5 — launch (in-repo)
172366
173367Run as a single Bash command so the discovered values survive into the
174368launch. Substitute ` EXTRA_DIFF_FILE ` and ` EXTRA_DIFF_LABEL ` literally with
220414# 3. Start the WS server (in-repo via tsx).
221415cd "$project_cwd" \
222416 && PORT=$port \
223- ASKDIFF_SESSION_ID="$session_id " \
417+ ASKDIFF_SESSION_ID="$attached_session " \
224418 ASKDIFF_PROJECT_CWD="$project_cwd" \
225419 ASKDIFF_DIFF_FILE="$EXTRA_DIFF_FILE" \
226420 ASKDIFF_DIFF_LABEL="$EXTRA_DIFF_LABEL" \
@@ -250,8 +444,9 @@ if ! $ui_running; then
250444fi
251445
252446# 5. Wait for Vite to print its "Local: http://localhost:XXXX/" line.
447+ # (`command grep` bypasses the harness's grep wrapper — see Step 4c.)
253448for _ in $(seq 1 60); do
254- grep -q "Local:" "$ui_log" 2>/dev/null && break
449+ command grep -q "Local:" "$ui_log" 2>/dev/null && break
255450 sleep 0.25
256451done
257452
@@ -279,7 +474,11 @@ echo "WS PID: $new_pid (saved to $pid_file)"
279474
280475Then tell the user:
281476- the WS server port (visible in the ` listening on ws://... ` line)
282- - the resolved Claude session ID (from the ` claude session: ` line)
477+ - the resolved Claude session ID (from the ` claude session: ` line) — and
478+ if ` $session_source ` is ` explicit ` or ` matched ` , say so explicitly
479+ (e.g. "attached to matched session 322bc90a (was: invoking)") so the
480+ user knows their asks are not landing in the current session's
481+ transcript
283482- the diff label (always set)
284483- the WS log file (printed as the ` WS log: ` line — ` /tmp/askdiff.<suffix>.log ` )
285484- the Vite log file (printed as the ` UI log: ` line — ` /tmp/askdiff-ui.<suffix>.log ` )
0 commit comments