repl: :diff meta + _git_clean_cmd helper (Phase 6 commit #3)

User-driven git diff injection. The model sees the diff on the next
ask_ai turn through the existing exec_output channel.

Changes:
  - _git_clean_cmd(subcmd_and_args) helper near _scan_project_tree.
    B1: every git invocation that flows into context MUST use
    `--no-pager -c color.ui=never`. Forkpty makes git think stdout
    is a TTY, enabling both color and the pager's keypad/line-clear
    escapes — these would pollute the captured context block. The
    helper is the single chokepoint; commit #4's @<r1>..<r2> retry
    will reuse it.

  - :diff [<args>] meta:
      - Reads cwd at meta invocation (R6: differs from :tree's
        scan-time cwd capture; documented in §5).
      - Runs `_git_clean_cmd("diff " .. args)` via executor.exec.
      - Empty output -> "(no diff): <label>" status, no context append.
      - Non-zero exit -> "diff failed (exit N): <label>" status,
        no context append. git's stderr already streamed to the
        user via executor.exec's live multiplex, so the failure
        reason is visible.
      - Success -> appends "[diff <label>]\n<output>" via
        ctx:append_exec_output. Label is "(working tree)" for empty
        args, else verbatim args.
      - Status confirms injection size: "diff injected: <label> (N bytes)".

  - HELP gains :diff line with three example arg shapes; N3-resolved
    (no `staged` alias — the meta is thin pass-through to git's grammar).

Smoke verified across four scenarios in an ephemeral test repo:
  - Working-tree dirty -> 110-byte diff injected, no ANSI escapes
  - --cached -> 118-byte staged diff injected, clean
  - garbage..nonexistent -> exit 128, status + skip
  - Clean working tree -> "(no diff)", status + skip

Regression: test_safety 87/87, test_router_model 31/31, repl loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 22:17:18 +00:00
parent d1dce832da
commit 4d5f93aaa5
+34
View File
@@ -151,6 +151,8 @@ Meta commands:
:tree [<depth>] scan cwd file-tree, inject as [project] block in system prompt
:tree refresh re-scan with last opts (or config defaults)
:tree off clear the [project] block
:diff [<git-args>] git diff <args> -> inject as [diff ...] exec_output
examples: :diff :diff --cached :diff main..feature
:delegate <p> <prompt> one-shot sub-broker call to preset <p>; prints reply
:help this message
]]
@@ -684,6 +686,15 @@ 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 <subcmd_and_args>`;
-- used by :diff and the @<r1>..<r2> @-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
-- Phase 6 (§6 + N4): project file-tree scanner. Prefers
-- `git -C <dir> ls-files --cached --others --exclude-standard`
-- when <dir> is inside a git repo (free .gitignore honor);
@@ -1763,6 +1774,29 @@ function M.run(config)
-- :tree <N> scan with depth=N; cached as _project_opts
-- :tree refresh re-scan with cached opts; else config defaults
-- :tree off clear ctx.project AND ctx._project_opts
-- Phase 6: :diff meta — `git diff <args>` (B1-clean), appends as
-- [diff <args>]\n<output> exec_output. Reads cwd at invocation
-- time (R6: differs from :tree's scan-time cwd capture). Empty
-- diff or git failure emits status and skips — never pollutes
-- context with empty or error noise.
meta.diff = function(args)
args = (args or ""):gsub("^%s+", ""):gsub("%s+$", "")
local cmd = _git_clean_cmd("diff " .. args)
local out, code = executor.exec(cmd)
if code ~= 0 then
renderer.status(("diff failed (exit %d): %s")
:format(code, args == "" and "(working tree)" or args))
return
end
if not out or out:gsub("%s", "") == "" then
renderer.status(("(no diff): %s"):format(
args == "" and "(working tree)" or args))
return
end
local label = args == "" and "(working tree)" or args
ctx:append_exec_output(("[diff %s]\n%s"):format(label, out))
renderer.status(("diff injected: %s (%d bytes)"):format(label, #out))
end
meta.tree = function(args)
local sub = (args or ""):match("^%s*(%S*)") or ""
if sub == "off" then