Skip to content

fix(runtime): harden the friction log command and stop copying Frog into every run - #1245

Merged
aviggiano merged 14 commits into
mainfrom
fix/1162-friction-log-hardening
Oct 1, 2026
Merged

aviggiano merged 14 commits into
mainfrom
fix/1162-friction-log-hardening

Conversation

@aviggiano

@aviggiano aviggiano commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #1162, which added the opt-in Frog friction log (run.friction_log_enabled, #172). Thanks @mrthankyou for the original work. This branch addresses the review of #1162.

Summary

  • The friction log command refuses incur's built-in flags, such as --mcp and --update, where an option value belongs.
  • It fences Git discovery with a GIT_DIR that never exists, so a : in the run path no longer sends entries into the target.
  • It unsets Frog's Postgres store variables and COMPLETE, sets NO_UPDATE_NOTIFIER=1, and gives Frog /dev/null as stdin.
  • Every task preparation rewrites the command, replacing a planted symlink, and prompt paths are inserted literally.
  • Runs no longer copy Frog (31 packages, 1,033 files, 20 MB) into their trusted CLI closure and execution snapshot.
  • Agents are told never to publish entries, and the docs now claim only what holds and say how to review entries.

Change

Refuse incur's built-in flags as option values (packages/runtime/src/friction-log.ts)

The wrapper skipped the token after --body, -b, --severity, -s, --label and --format without reading it. incur, the CLI framework Frog is built on (0.4.25, which frog 1.1.0 pins exactly), takes its built-in flags from every argv position (extractBuiltinFlags). So on the base:

  • log x --body --mcp started Frog's MCP server, whose search_tools finds publish;
  • log x --body --update installed the latest Frog globally (pnpm add --global frog@latest, recorded by a stand-in pnpm);
  • --body --llms, -s --schema and --label --help printed Frog's manifest, schema or help instead of logging.

The wrapper now exits 2 when an option value is exactly one of incur's 16 built-in tokens (INCUR_BUILTIN_FLAGS). incur has no = forms for them, and Frog enables no config flag. Any other value passes, so a body that starts with - still logs.

The log --format=* and list --format=* patterns are gone. incur reads --format only with a separate value and rejects --format=json with Unknown flag: --format, so the wrapper now refuses those forms itself.

Fence Git discovery with GIT_DIR (friction-log.ts)

GIT_CEILING_DIRECTORIES is a :-separated list. For a project such as a:b-target/, the run root was therefore no ceiling: discovery reached the target's .git, and Frog wrote a:b-target/.agents/friction-log/<id>/friction.md. The wrapper now exports GIT_DIR=<run>/friction-bin/no-git, a path nothing creates, and still unsets GIT_WORK_TREE. Git then does no discovery, the two Git commands Frog runs fail, and Frog uses its --cwd, <run>/friction.

Environment and stdin (friction-log.ts)

  • Unsets FROG_DATABASE_URL, FROG_NAMESPACE and FROG_SCHEMA. FROG_DATABASE_URL sends log and list to that Postgres database.
  • Unsets COMPLETE. While it is set, incur prints shell completions instead of running the command, so log exited 0 and wrote no entry. _COMPLETE_INDEX is read only while COMPLETE is set.
  • Sets NO_UPDATE_NOTIFIER=1. Nothing in Frog's dependency chain uses the update-notifier package, but incur's own update check, which runs when stdout is a terminal, honours the variable.
  • Gives Frog /dev/null as stdin. With a terminal on stdin and stderr, log prompts for a missing title or severity and, without a body, opens $EDITOR.
  • Exports GH_CONFIG_DIR=<run>/friction-bin/no-gh. Frog's last token source is gh auth token. A missing config directory hides a token that gh keeps in its config file (gh 2.101.0: no oauth token found for github.com), but gh's ActiveToken then falls back to the system keyring. So the wrapper removes no credential, and no comment or doc now says it does.

Stop copying Frog into every run (packages/runtime/src/trusted-cli-closure.ts, packages/runtime/src/smithers.ts)

Both dependency walks skip a first-party package's frog edge, beside the existing smthrs skip. The command execs the Frog that resolveFrogBin() resolved in the process that rendered the workflow, so the copies never ran. Modal workers run their own install in /opt/ultrafuzz, and per-node cloud execution has been removed. postgres stays, because pnpm links it as drizzle-orm's optional peer.

Measured at this branch's head. The "before" rows run the same code with the Frog edge followed again:

Copy Packages Files Bytes
Trusted CLI closure, before 374 16,683 214,087,968
Trusted CLI closure, after 343 15,650 193,999,337
Execution snapshot dependencies, before 275 12,655 170,666,870
Execution snapshot dependencies, after 244 11,622 150,578,239

Each copy loses the same 31 packages, 1,033 files and 20,088,631 bytes. Frog and incur are gone from both; postgres remains.

Rewrite the command on every preparation (packages/runtime/src/templates/smithers/workflows/workflow.tsx, prepareFrictionLog)

Before, preparation left anything other than a regular file at <run>/friction-bin/ultrafuzz-friction-log in place, and rewrote a regular file only when its bytes differed. A planted symlink therefore stayed, and the next agent that followed its prompt ran whatever it named. Every preparation now stages the wrapper (O_EXCL) and renames it into place. The lstat and the read-compare are gone. A directory at that path cannot be replaced: preparation reports that on stderr, removes the staged copy, and the task continues.

Tell agents never to publish entries (.ultrafuzz/prompts/_templates/agent-preamble/friction-log.mdx)

Adds "Never publish entries; an operator reviews them first." With the example paths of its test, the rendered fragment grows from 919 to 974 bytes, within its 1,536-byte budget. pnpm -w docs:prompt-catalog regenerates the catalog with no diff.

Insert prompt paths literally (workflow.tsx, renderAgentPrompt)

The two replaceAll calls for {{friction_log_command}} and {{friction_log_directory}} now pass replacer functions, as the template substitution after them already does. $$, $&, $` and $' in a run root are no longer expanded.

Tests

  • packages/runtime/test/workspace-preparation-lifecycle.test.ts: a new case shows that prepareArtifactMirror installs an enabled friction log. Before, deleting the prepareFrictionLog(task) call failed no test.
  • packages/runtime/test/generated-workflow-verifier.test.ts: drops the tautological "a task without a friction log renders exactly what a disabled run renders", whose two sides ran the same code. Adds a run root with $$ and $&. The planted-symlink case now expects the command to be replaced, and a new case covers a directory that cannot be replaced. The rerender harness now checks that the friction directory reaches every agent instance.
  • packages/runtime/test/friction-log.test.ts:
    • The refusal table passes each of the 16 built-in flags as a --body value. It also asserts that the incur next to the pinned Frog is 0.4.25, so an upgrade that brings another incur fails until someone rechecks the list.
    • A run root containing : logs and lists, and nothing lands in the target.
    • A recorder in place of Frog checks the arguments, environment and stdin Frog receives. It replaces the assertion on the script's exact text.
  • packages/runtime/test/runtime.test.ts and packages/runtime/test/trusted-cli.test.ts assert that the execution snapshot and the trusted CLI closure leave out Frog. Regexes that pinned function signatures and declarations are replaced with behaviour checks where that was practical.
  • The e2e campaign still runs with the friction log disabled.

Docs and CHANGELOG (docs/reference/configuration.md, CHANGELOG.md)

The configuration reference and the CHANGELOG entry now say:

  • The command guards against misuse by mistake and enforces nothing. Agents run unsandboxed (docs/security.md), so <run>/friction is where the command writes, not a boundary, and the command removes no GitHub credential.
  • Entries are free-form agent text that the artifact secret scan does not cover. Check them for credentials, such as RPC URLs with API keys, before publishing.
  • To review entries, read the Markdown. To list them with Frog, run the Frog pinned in the Ultrafuzz checkout with GIT_DIR set to a path that does not exist, never a bare npx frog. Without the fence, list --cwd <run>/friction lists the target repository's log instead.
  • Frog refuses every log and list while one entry is malformed. Deleting that entry's directory recovers.

The CHANGELOG's #1201 entry gains one sentence. Frog's postgres satisfies drizzle-orm's optional postgres peer, which pnpm records in the dependency paths of the Smithers packages that reach drizzle-orm, the engine's included. So the pnpm install that adds Frog moves the engine: let campaigns finish, or pause them, before that install, and ultrafuzz resume a paused one afterwards.

Review follow-ups on this branch

  • COMPLETE is unset, as described above.
  • The refusal test covers all 16 built-in flags. Before, deleting any of 12 of them from the wrapper, --update among them, failed no test.
  • The CHANGELOG entry gained the GIT_DIR fence in its review advice, GH_CONFIG_DIR with the keyring reason, "where an option value belongs" (--format is a built-in flag the command accepts as an option), and "the Smithers packages that reach drizzle-orm". Those are 25 of the 48 smthrs and @smthrs/* lockfile snapshots: 24 gain a ([email protected]) suffix, and the hashed peer suffix of @smthrs/testing changes.
  • friction-log.ts, the configuration reference and the CHANGELOG said that <run>/friction is where agents are told to write. They are told the opposite: to leave it to the command.
  • One commit message's byte counts are corrected to 919 and 974.

Verification

All on this branch's head, from a clean build: every dist and dist-test directory and the generated dashboard sources removed, then pnpm install --frozen-lockfile --prefer-offline (already up to date).

Command Result
pnpm -w build pass
pnpm -w typecheck pass, all 12 packages
pnpm -w lint pass
CI=1 ESLINT_PLUGIN_DIFF_COMMIT=f13850e5 pnpm -w lint:strict:ci pass
pnpm -w knip pass
pnpm -w format:check pass
pnpm -w docs:check pass
pnpm -w docs:prompt-catalog regenerates docs/reference/prompt-catalog.md with no diff
eatmydata pnpm --filter @ultrafuzz/runtime test:release:supporting Node 948/948; Bun 1.3.14 adapter contracts 53 pass, 1 skip, 0 fail
eatmydata node scripts/run-runtime-test-shard.mjs N/4 for N = 1..4 in packages/runtime, in parallel, on the dist-test that run compiled 89 + 86 + 90 + 77 = 342 pass, 0 fail
npx vitest run --testTimeout=30000 in packages/prompts 60/60
tsc -p tsconfig.test.json && eatmydata node --test dist-test/test/e2e/*.test.js in packages/cli (test:e2e without its build step, which the clean build had done) 1/1

Checked against the real frog 1.1.0 and incur 0.4.25 in scratch fixtures outside the repository:

  • With the base fence (GIT_CEILING_DIRECTORIES set to the run root) and a : in the path, Frog wrote the entry to a:b-target/.agents/friction-log/.
  • Given the argv the base wrapper passed through (log --cwd <run>/friction x --body <flag>), --mcp served an MCP server whose search_tools returned publish, --update ran pnpm add --global frog@latest (a stand-in pnpm recorded the call), and --llms printed Frog's manifest.
  • Before COMPLETE was unset, the command exited 0 with COMPLETE=bash, printed nothing and wrote no entry.
  • Under a pty without --body, Frog run directly opened $EDITOR on the new entry; through the command it did not, and log failed with MISSING_BODY. With a terminal on stdout, Frog run directly wrote incur/updates/frog.json under XDG_CACHE_HOME; through the command it wrote nothing.
  • GH_CONFIG_DIR=<missing path> gh auth token exits 1 on this host, whose token is file-stored (gh 2.101.0). gh v2.101.0's source still reads the keyring in that case (ActiveToken falls back to TokenFromKeyring).
  • list --cwd <run>/friction without GIT_DIR printed entries: [], the target's empty log; with GIT_DIR=/nonexistent it listed the run's entries.
  • One malformed entry made both list and log fail with MALFORMED_ENTRY; deleting its directory recovered.

Real engine: a smoke campaign on a small Foundry vault with friction_log_enabled = true, on 19459fc4 (this branch before its review follow-ups, which change the wrapper only by also unsetting COMPLETE), finished succeeded with a verified, complete report in 11m 33s. During it an agent recorded one entry through the command, under <run>/friction/.agents/friction-log/. Afterwards, list and an operator log worked; publish, log x --body --mcp, --body --llms, -s --schema, --label --help, --label -h, --cwd /tmp and a trailing --label all exited 2; the target root had no .agents; and the run held no copy of Frog. The same campaign on main before this branch also succeeded, but there the five flag-in-value cases exited 0 and the run held two copies of Frog.

Mutation checks: each change below was applied on its own, its test run, and the change reverted. Every one fails its test.

  • Each of the 16 deletions from INCUR_BUILTIN_FLAGS fails the refusal test. Against the refusal table before this branch's review follow-ups, 12 of them passed.
  • Wrapper: GIT_CEILING_DIRECTORIES instead of GIT_DIR; GIT_DIR not exported; GIT_WORK_TREE, the FROG_* variables or COMPLETE not unset; no GH_CONFIG_DIR; no NO_UPDATE_NOTIFIER; stdin not /dev/null; option values not checked.
  • Dependency walks: the frog edge followed in the trusted CLI closure (trusted-cli.test.ts) or in the execution snapshot (startRun installs, seals, and revalidates operator-owned Smithers).
  • Workflow template: the prepareFrictionLog(task) call removed from prepareArtifactMirror; a replacement string instead of a replacer function; the staged copy left behind after a failed rename; an existing command left in place; the friction directory missing from addDir.

Measurements: the trusted CLI closure comes from prepareTrustedCliEnvironment on packages/cli/dist/index.js, and runTrustedJsonValidatorPreflight passes on it. The execution snapshot comes from a scratch ultrafuzz run with SMITHERS_BIN set to a stand-in Smithers. For the "before" rows, the frog condition was removed from a copy of the closure code and from the built smithers.js, which was restored afterwards.

Not in this PR

  • run.frictionLogEnabled stays optional in the resolved-config schema (packages/config/src/resolved-config-schema.ts:302, packages/config/src/types.ts:90-91) on purpose. Requiring it would make every run sealed before feat(runtime): let agents record roadblocks in a run-local friction log #1162 fail to resume, for no gain.
  • The command still removes no credential: gh reads a token kept in the keyring, and agents can run gh or Frog directly. Making credentials unavailable to agents needs a sandbox, not this wrapper.
  • Piped input (cat entry.md | ... log) no longer works through the command, because Frog reads /dev/null. The prompt never uses it.
  • Only the version pin in the test keeps INCUR_BUILTIN_FLAGS in step with incur.
  • The e2e campaign keeps the friction log disabled.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable regression was established.

Summary

The PR hardens the opt-in friction-log command and removes unused copies of Frog from run dependency snapshots.

  • It rejects incur built-in flags in option-value positions and isolates Frog’s Git, store, completion, update-check, and stdin behavior.
  • It replaces the command during each task preparation, inserts prompt paths literally, and clarifies operator review guidance.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Workflow render] --> B[Resolve installed Frog and seal wrapper]
  B --> C[Task preparation]
  C --> D[Replace run-local command]
  D --> E[Agent invokes log or list]
  E --> F[Validate arguments and set environment]
  F --> G[Installed Frog writes under run friction directory]
Loading

Reviews (1) · Last reviewed commit: "docs: say the friction log command write..."

aviggiano and others added 14 commits October 1, 2026 12:09
…lues

The friction log wrapper skips the token after a value-taking option
(--body, -b, --severity, -s, --label, --format) without looking at it.
incur, the CLI framework Frog is built on, extracts its built-in flags
from every argv position before Frog parses options
(extractBuiltinFlags in incur 0.4.25), so `log x --body --mcp` started
Frog's MCP server, which serves every Frog command including `publish`,
and `--body --llms`, `-s --schema` and `--label --help` printed Frog's
manifest, schema and help instead of logging.

The wrapper now refuses, with exit 2, an option value that is exactly one
of incur's built-in flags: --full-output, --llms, --llms-full, --mcp,
--help, -h, --update, --incur-update-check, --version, --schema, --json,
--format, --filter-output, --token-limit, --token-offset and
--token-count. Any other value is still passed through, so a body that
starts with "- " keeps working: incur's option parser takes the next
token as the value whatever it starts with.

It also drops the `log --format=*` and `list --format=*` patterns:
incur reads --format only with a separate value and the command parsers
reject `--format=json` as "Unknown flag: --format", so those forms could
never work. The wrapper now refuses them itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…hat never exists

Frog anchors its entries at `git rev-parse --show-toplevel`, and run roots
sit inside the target repository. The wrapper stopped discovery with
GIT_CEILING_DIRECTORIES=$run_root, but Git splits that variable at `:`,
so for a project such as `a:b-target/` the run root was no ceiling:
discovery reached the target's `.git` and Frog wrote the entry to
`a:b-target/.agents/friction-log/` in the target checkout.

The wrapper now exports GIT_DIR=$run_root/friction-bin/no-git, a path
preparation never creates, and still unsets GIT_WORK_TREE. With GIT_DIR
set Git does no discovery, and because it names nothing both Git commands
`log` and `list` run (`rev-parse --show-toplevel` and
`remote get-url origin`) fail, so Frog falls back to its --cwd,
<run>/friction. A new test logs and lists with a run root containing `:`;
it fails against the old ceiling.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ut of the friction log

The friction log wrapper unset GITHUB_TOKEN, GH_TOKEN and GITHUB_API_URL
and claimed that removed GitHub credentials. It does not: Frog's last
token source is `gh auth token`, and HOME reaches agents. Other inherited
state also reached Frog:

- FROG_DATABASE_URL (with FROG_NAMESPACE and FROG_SCHEMA) makes `log` and
  `list` use that Postgres database instead of <run>/friction. The
  wrapper now unsets all three.
- With a terminal on stdin and stderr, `log` prompts for a missing title
  or severity and, without --body, opens $EDITOR on the entry (observed
  under a pty). Frog now gets /dev/null as stdin, so it never does.
- With a terminal on stdout, incur's update check writes
  ~/.cache/incur/updates/frog.json and fetches Frog's latest version from
  the npm registry in a detached process (observed). This is incur's own
  check, not the update-notifier package, but it honours the same
  NO_UPDATE_NOTIFIER variable, which the wrapper now sets.

The wrapper also exports GH_CONFIG_DIR=$run_root/friction-bin/no-gh, a
path that never exists. With gh 2.101.0 that makes `gh auth token` fail
when gh keeps its token in its config file, as on a headless host, but gh
also reads the active token from the system keyring (ActiveToken falls
back to the keyring entry for the host), so this narrows the fallback
and removes no credential. The comments no longer claim otherwise, or
that agents cannot replace the wrapper: agents run unsandboxed. What
keeps entries local is that no command the wrapper accepts publishes or
reads from GitHub.

The wrapper test no longer pins the script's text. It runs the wrapper
with a recorder in place of Frog, from two run roots, with the variables
above inherited and input piped in, and checks the arguments,
environment and stdin Frog receives. The real-Frog test logs with a
FROG_DATABASE_URL inherited.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… and execution snapshot

@ultrafuzz/runtime depends on frog, so every run copied Frog's package
closure into its trusted CLI closure and into each execution snapshot,
even with the friction log disabled. Neither copy ever runs: the friction
log wrapper embeds the path resolveFrogBin() returns in the process that
renders the workflow (launch, or `resume --refresh-controller`), which is
the Frog of that Ultrafuzz install. Nothing loads Frog from a run's
copies: friction-log.js only resolves it inside resolveFrogBin(), which
only renderWorkflowSource calls; the validator launcher and the sealed
workflow never render a workflow, and per-node cloud execution, the only
remote mode, was removed. Modal workers launch from their own checkout in
/opt/ultrafuzz, so they too run that install's Frog.

Both walks now skip a first-party package's `frog` edge, beside the
existing skip of its `smthrs` edge, so they also drop everything only
Frog reaches. Measured on this checkout:

- trusted CLI closure: 374 -> 343 packages, 16,683 -> 15,650 files,
  214,083,380 -> 193,998,957 bytes;
- execution snapshot (local launch, SMITHERS_BIN runner): 275 -> 244
  dependency packages, 13,606 -> 12,573 files, 181,169,679 ->
  161,072,579 bytes.

That is 31 packages, 1,033 files and about 20 MB per copy. `postgres`
stays: since Frog brought it into the install, pnpm links it as
drizzle-orm's optional peer, which both walks follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
prepareFrictionLog left whatever sat at <run>/friction-bin/
ultrafuzz-friction-log alone when it was not a regular file, and rewrote
a regular file only when its bytes differed. A symlink planted there
therefore stayed in place, and the next agent that followed its prompt
ran whatever the symlink named.

Every preparation now stages the wrapper (O_EXCL, so the staging name is
never followed) and renames it into place. rename(2) replaces the entry
itself, so a symlink or any other file at that path is replaced rather
than followed or left to run, and tasks preparing in parallel still
never see a partial file. A directory at that path cannot be replaced:
the failure is reported on stderr as before and never fails the task,
and the staged copy is removed so a persistent failure does not leave
one per attempt. The lstat and the read-compare are gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
renderAgentPrompt passed the friction log command and directory to
String.prototype.replaceAll as replacement strings, so `$$`, `$&`, `` $` ``
and `$'` in a run root were expanded: `$$` became `$` and `$&` became the
placeholder itself, and the prompt named a command and directory that do
not exist. They now go through replacer functions, as the template
substitution below them already does. A new test renders a run root
containing `$$` and `$&`; it fails with replacement strings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The friction log instructions told agents how to record and list
entries, and never to edit them by hand, but not to leave publishing
alone. The wrapper refuses Frog's publishing commands and options, but
agents run unsandboxed and can run Frog or `gh` directly, so the
instruction is what asks them not to. Entries are free-form agent text
an operator reviews before anything leaves the run. With the example
paths of its test the rendered fragment grows from 919 to 974 bytes,
within its 1,536-byte budget, and the prompt catalog is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… behaviour, not its text

Deleting the prepareFrictionLog(task) call from prepareArtifactMirror
failed no test: the preparation lifecycle harness always ran with a
disabled friction log. It now takes an optional friction log (and the
randomUUID the preparation stages with), and a new case asserts that
preparing a task with one enabled creates <run>/friction and installs the
command. Deleting the call fails that case.

The verifier's "a task without a friction log renders exactly what a
disabled run renders" compared renderAgentPrompt with itself: both sides
take the same `friction === undefined` branch. It is gone, with the "only
when a task has one" of its test's name; the existing exact-output render
test guards the disabled path, and in an enabled run every task has a
friction log.

Assertions that pinned the friction log's source text are replaced with
behaviour where that was practical:

- the frictionLogPaths signature regex: the preparation test now checks
  that a relative run root resolves against the working directory;
- the untrusted_content_boundary concatenation regex: the render test
  already checks the fragment's position after the trust boundary;
- the `...frictionLogAddDir(task)` addDir regexes: the rerender harness
  now passes a friction directory and checks that every agent instance
  gets it after its other roots, and the regexes check only the part they
  pinned before the friction log existed;
- the `const frictionLog: {...} | null =` declaration regexes: the
  runtime test reads the sealed value whatever the declaration's type
  annotation says.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…iew entries

The friction log docs and CHANGELOG entry promised more than the command
does. Agents run unsandboxed (Claude with bypassPermissions, Codex with
--dangerously-bypass-approvals-and-sandbox; docs/security.md), so
`<run>/friction` is where agents are told to write, not a boundary, and
the wrapper guards against misuse by mistake rather than enforcing
anything. It removes no GitHub credential either: Frog falls back to
`gh auth token`, and gh reads a keyring-stored token whatever
GH_CONFIG_DIR says.

The configuration reference and the CHANGELOG entry now describe the
command as it is after this series: the accepted options, the refusal
of incur's built-in flags as option values, the GIT_DIR fence, the
variables it unsets, NO_UPDATE_NOTIFIER, /dev/null on stdin, the
rewrite on every preparation, and that runs no longer copy Frog. They
say entries are free-form agent text the artifact secret scan does not
cover, so operators should check them for credentials such as RPC URLs
with API keys before publishing. Review leads with reading the Markdown
files; the documented Frog command uses the Frog pinned in the
Ultrafuzz checkout with GIT_DIR naming a path that does not exist,
never a bare `npx frog`, which can run the target's own
node_modules/.bin/frog or fetch an unpinned Frog. A malformed entry
makes Frog refuse every `log` and `list`; deleting its directory
recovers (both checked against frog 1.1.0).

The #1201 entry, which already explains what a `pnpm install` that
moves the engine does to live runs, now says the install that adds Frog
is one: Frog's `postgres` satisfies drizzle-orm's optional peer, which
pnpm records in the dependency paths of `smthrs` and the `@smthrs/*`
packages that reach drizzle-orm (24 lockfile snapshots gain a
`([email protected])` suffix; the engine's directory hash changes from
a66d2d2a... to b64bd027...). Operators should let campaigns finish or
pause them first, and resume a paused one afterwards; a plain
`ultrafuzz resume` suffices, because it resolves the installed engine in
its own process.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… they are

Three comments still overstated what the friction log does. The wrapper
accepts a few of `log`'s and `list`'s options, not all of their local
ones; agents are told to record entries only through the wrapper rather
than doing so by construction; and preparation replaces a file or a
symlink at the wrapper's path but cannot replace a directory, which it
reports instead.

The check that nothing creates the paths GIT_DIR and GH_CONFIG_DIR name
moves from the recorder test, where nothing runs Git, to the real-Frog
test with a `:` in the run root, where Frog's Git commands run against
that GIT_DIR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ion value

The refusal table passed only 4 of incur's 16 built-in flags where an
option value belongs (--llms, --schema, --help and --mcp). Deleting any
of the other 12 from INCUR_BUILTIN_FLAGS failed no test, --update among
them, although `log x --body --update` then reaches incur, which
installs the latest Frog globally (from this pnpm install it ran
`pnpm add --global frog@latest`, observed with a stand-in pnpm on PATH).

The test now passes each of the 16 flags as the value of --body and
expects the refusal; deleting any one of them from the wrapper fails it
(checked for each). It also asserts that the incur next to the pinned
Frog is 0.4.25, the version whose extractBuiltinFlags the list was
taken from, so an upgrade that brings another incur fails the test
until someone checks the list against it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
incur, Frog's CLI framework, checks COMPLETE before it runs a command:
while it is set, incur prints shell completions and returns. So with
COMPLETE in the environment, `log` exited 0 with no output and wrote no
entry (observed with COMPLETE=bash), and the agent saw success. The
wrapper now unsets it with the other inherited variables.
_COMPLETE_INDEX, incur's only other completion variable, is read only
while COMPLETE is set.

The real-Frog test logs with COMPLETE=bash inherited, and the recorder
test checks that Frog never sees it; removing the unset fails both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…reference

The entry told operators to read entries "with the Frog pinned in the
Ultrafuzz checkout" but left out the GIT_DIR fence. Followed as written,
`frog list --cwd <run>/friction` lets Git discovery climb from the run
to the target repository's root and lists the target's log instead
(`entries: []` for a target without one); with GIT_DIR=/nonexistent it
lists the run's entries (both checked against frog 1.1.0). It now names
the fence and points to docs/reference/configuration.md, which gives
the command.

Three other statements were looser than the reference:

- "refuses the built-in flags of incur ... even as option values" reads
  as if every built-in flag were refused, but `--format` is one and the
  command accepts it as an option. It now says "where an option value
  belongs", as the reference does.
- It gave `gh auth token` as the reason no credential is removed but
  did not say the command points GH_CONFIG_DIR at a missing path, which
  does hide a token gh keeps in its config file. It now does, and gives
  the reason that remains: `gh auth token` still finds a token kept in
  the system keyring (gh v2.101.0's ActiveToken falls back to the
  unkeyed keyring slot that `gh auth login` fills).
- "moves the Smithers packages' install paths": only the Smithers
  packages that reach drizzle-orm move, 25 of the 48 smthrs and
  @smthrs/* lockfile snapshots (24 gain a `([email protected])` suffix and
  @smthrs/testing's hashed peer suffix changes). It now says so, as the
  #1201 entry above does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three places described <run>/friction as where agents are told to
write: the comment above the run-relative paths in friction-log.ts
("the one run directory agents are told to write"), the configuration
reference and the CHANGELOG entry. Agents are told the opposite: to
record friction by running the command, and never to create, edit or
delete anything under <run>/friction themselves (friction-log.mdx).
Their own outputs go under {{artifact_path}}, the attempt's artifact
directory. The configuration reference contradicted itself a few lines
later.

They now say what happens: the command writes entries there, and agents
are told to leave the directory to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@aviggiano
aviggiano requested a review from a team as a code owner October 1, 2026 12:09
@aviggiano
aviggiano merged commit 16990bc into main Oct 1, 2026
17 checks passed
@aviggiano
aviggiano deleted the fix/1162-friction-log-hardening branch October 1, 2026 12:50
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