From 0d63f01601db4da05c7d74f6b99f8f6a45b07d37 Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sat, 16 May 2026 22:20:25 +0000 Subject: [PATCH] repl: expand_mentions tiered @.. diff retry (Phase 6 commit #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per A6 (tiered resolution): @ tries file lookup first; if the file doesn't exist AND the token contains "..", retry as a git ref-range and substitute with a fenced `diff` block. Preserves the existing peel-on-trailing-punct logic (e.g., `@HEAD~1..HEAD,` peels the comma, resolves the ref, restores the comma after the closing fence). Resolution order for @: 1. io.open(token, "rb") -- file lookup, with trailing-punct peel 2. if (1) fails and token contains "..": git --no-pager -c color.ui=never diff .. on exit 0 + non-empty body: substitute as ```diff fenced block 3. else: leave literal `@token` + emit "[aish] @X: not found" status Examples: @README.md -> file (path branch) @../sibling.txt -> file (path branch; `..` only triggers retry when path lookup FAILS, so existing paths with `..` segments are unaffected) @HEAD~1..HEAD -> diff (path fails, ref succeeds) @origin/main..feature -> diff (path fails — no such literal file; ref succeeds; `/` in ref is fine because we don't use the path's `/`-absence as a discriminator) @nonsense..gibberish -> literal preserved (both fail) Required restructuring: - _shq and _git_clean_cmd lifted from M.run closure scope to module scope (above expand_mentions). Single source of truth for the B1 prefix shared with commit #3's :diff. The in-M.run duplicates are removed. - expand_mentions now references `executor` (already required at module scope on line 7) for the diff retry. Status messages updated: - File expansion: "@ expanded (N bytes, truncated)" (existing) - Diff expansion: "@ expanded (N bytes, diff)" (new) Tested with the 7 existing #7 cases + 7 new diff-retry cases (14/14): ref-range expansion shape, body contains `diff --git`, trailing prose preserved, @../path stays as file (not diff), neither-path- nor-ref preserves literal, trailing-comma peel composes with ref retry. Regression: test_safety 87/87, test_router_model 31/31, repl loads. Co-Authored-By: Claude Opus 4.7 (1M context) --- repl.lua | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/repl.lua b/repl.lua index dbb0c3c..4a55727 100644 --- a/repl.lua +++ b/repl.lua @@ -45,6 +45,16 @@ local function _read_truncated(path) .. ("\n... [%d bytes elided] ...\n"):format(#content - MENTION_HEAD - MENTION_TAIL) .. tail, true end + +-- ---------------------------------------------------------------- shared shell helpers +-- Lifted from M.run closure scope so expand_mentions (module-scope) can also +-- use them for the @.. diff-retry path. Same single source of truth +-- for the B1 git invocation prefix; commits #3 and #4 both call _git_clean_cmd. +local function _shq(s) return "'" .. (s or ""):gsub("'", [['\'']]) .. "'" end +local function _git_clean_cmd(subcmd_and_args) + return "git --no-pager -c color.ui=never " .. subcmd_and_args +end + local function expand_mentions(line, on_status) -- Walk the line; for each "@" preceded by SOL or whitespace, -- attempt to read and substitute. Missing files leave the literal @@ -72,13 +82,35 @@ local function expand_mentions(line, on_status) end if path ~= "" then local content, truncated = _read_truncated(path) + local lang_override = nil + -- Phase 6 / A6: tiered resolution — if path lookup + -- failed AND token contains "..", try as a git diff + -- ref-range. `@HEAD~1..HEAD` and `@origin/main..feature` + -- both fall through to this branch when no such file + -- exists. `@../sibling.txt` resolves as path first + -- and never reaches this retry. + if not content and path:find("..", 1, true) then + local r1, r2 = path:match("^(.-)%.%.(.+)$") + if r1 and r2 and r1 ~= "" and r2 ~= "" then + local out_diff, code = executor.exec( + _git_clean_cmd(("diff %s..%s 2>/dev/null") + :format(_shq(r1), _shq(r2)))) + if code == 0 and out_diff and out_diff:match("%S") then + content = out_diff + lang_override = "diff" + end + end + end if content then + local lang = lang_override or _lang_of(path) if on_status then on_status(("@%s expanded (%d bytes%s)"):format( - path, #content, truncated and ", truncated" or "")) + path, #content, + truncated and ", truncated" + or (lang_override == "diff" and ", diff" or ""))) end out[#out + 1] = ("```%s path=%s\n%s\n```%s"):format( - _lang_of(path), path, content, trail) + lang, path, content, trail) i = path_end else if on_status then @@ -669,7 +701,8 @@ function M.run(config) -- receives the command on stdin and AISH_CMD/AISH_TURN/AISH_CWD as -- env vars. Non-zero exit on pre_cmd aborts. post_cmd exit is -- ignored; its stdout is logged via renderer.status. - local function _shq(s) return "'" .. (s or ""):gsub("'", [['\'']]) .. "'" end + -- _shq lifted to module scope (above expand_mentions) so the + -- @-mention diff retry can share the same quoter. local function _run_hook(script, cmd, want_output) local cwd = (require("ffi.libc").getcwd()) or os.getenv("PWD") or "?" local pipeline = string.format( @@ -686,14 +719,10 @@ function M.run(config) end end - -- Phase 6 (B1): every git invocation that flows back into context - -- MUST suppress git's interactive pager + color output. Forkpty - -- makes git think stdout is a TTY, which enables both. Helper - -- prepends `git --no-pager -c color.ui=never `; - -- used by :diff and the @.. @-mention path in commit #4. - local function _git_clean_cmd(subcmd_and_args) - return "git --no-pager -c color.ui=never " .. subcmd_and_args - end + -- _git_clean_cmd lifted to module scope (above expand_mentions); + -- shared with the @.. @-mention diff retry. Same B1 + -- invariant: every git invocation that flows back into context + -- runs with `--no-pager -c color.ui=never`. -- Phase 6 (§6 + N4): project file-tree scanner. Prefers -- `git -C ls-files --cached --others --exclude-standard`