pi-fff: emit hashline [path#TAG] headers so ffgrep output is directly editable in omp
Summary
ffgrep / fffind output carries no hashline [path#TAG] header, so in oh-my-pi (omp) every fff hit needs a follow-up native read before edit can anchor. That follow-up round trip spends most of the token saving fff just earned.
The fix needs no session access — omp's hashline tag is derived from file bytes alone. pi-fff can mint it locally.
Why this is cheap
Tag recipe (@oh-my-pi/hashline/src/format.ts:108-121):
const norm = text.replace(/[ \t\r]+(?=\n|$)/g, "");
const tag = (Bun.hash.xxHash32(norm, 0) & 0xffff).toString(16).padStart(4, "0").toUpperCase();Apply gate consults no snapshot store (@oh-my-pi/hashline/src/patcher.ts:688,728):
const liveMatches = expected !== undefined && computeFileHash(normalized) === expected;
if (expected === undefined || liveMatches) { /* applyEdits */ }Verified empirically 2026-08-17 on omp 0.10.5-era build: hand-computed a tag (DD92) in a REPL for a file the agent had never read, issued [path#DD92] PUT 2.=2: and the edit applied, returning a fresh tag. So emitting [relpath#TAG] plus N: text rows from the bytes fff already has in hand makes fff output directly editable.
Proposed change
When the host is omp (or unconditionally, behind a flag), prefix each file's match group in ffgrep output with [<relpath>#<TAG>] and format rows as N: text, matching omp's native grep/read shape (src/tools/grep.ts:1466 mints the same header).
Caveat worth encoding
If edit.enforceSeenLines is true (default false, settings-schema.ts:3239-3241), the stricter guard additionally wants seen-line provenance via recordSeenLinesFromBody (src/edit/file-snapshot-store.ts:141). That lives on AgentSession.fileSnapshotStore, and ExtensionContext exposes sessionManager, modelRegistry, cwd, ui, model, models — never the session (extensibility/extensions/runner.ts:1045-1081). So an extension cannot register provenance.
Fallback for that configuration only: register under the builtin name (override mode) and delegate to the native implementation via ctx.invokeTool, which is wired only when the extension tool name shadows a builtin (extensibility/extensions/types.ts:497-509, wrapper.ts:69-86). Note override mode is currently affected by #792.
Prior art
Searched both dmtrKovalenko/fff and can1357/oh-my-pi — 0 hits for the tag gap. Related but distinct: #737 / PR #741 (dynamic ${toolNames.grep} in promptGuidelines), #792 / PR #793 (tools registered before startup mode known).
Source: dmtrKovalenko/fff