fix(runtime): harden the friction log command and stop copying Frog into every run - #1245
Merged
Merged
Conversation
…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]>
This was referenced Oct 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
--mcpand--update, where an option value belongs.GIT_DIRthat never exists, so a:in the run path no longer sends entries into the target.COMPLETE, setsNO_UPDATE_NOTIFIER=1, and gives Frog/dev/nullas stdin.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,--labeland--formatwithout 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 --mcpstarted Frog's MCP server, whosesearch_toolsfindspublish;log x --body --updateinstalled the latest Frog globally (pnpm add --global frog@latest, recorded by a stand-inpnpm);--body --llms,-s --schemaand--label --helpprinted 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=*andlist --format=*patterns are gone. incur reads--formatonly with a separate value and rejects--format=jsonwithUnknown flag: --format, so the wrapper now refuses those forms itself.Fence Git discovery with
GIT_DIR(friction-log.ts)GIT_CEILING_DIRECTORIESis a:-separated list. For a project such asa:b-target/, the run root was therefore no ceiling: discovery reached the target's.git, and Frog wrotea:b-target/.agents/friction-log/<id>/friction.md. The wrapper now exportsGIT_DIR=<run>/friction-bin/no-git, a path nothing creates, and still unsetsGIT_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)FROG_DATABASE_URL,FROG_NAMESPACEandFROG_SCHEMA.FROG_DATABASE_URLsendslogandlistto that Postgres database.COMPLETE. While it is set, incur prints shell completions instead of running the command, sologexited 0 and wrote no entry._COMPLETE_INDEXis read only whileCOMPLETEis set.NO_UPDATE_NOTIFIER=1. Nothing in Frog's dependency chain uses theupdate-notifierpackage, but incur's own update check, which runs when stdout is a terminal, honours the variable./dev/nullas stdin. With a terminal on stdin and stderr,logprompts for a missing title or severity and, without a body, opens$EDITOR.GH_CONFIG_DIR=<run>/friction-bin/no-gh. Frog's last token source isgh 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'sActiveTokenthen 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
frogedge, beside the existingsmthrsskip. The command execs the Frog thatresolveFrogBin()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.postgresstays, 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:
Each copy loses the same 31 packages, 1,033 files and 20,088,631 bytes. Frog and incur are gone from both;
postgresremains.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-login 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. Thelstatand 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-catalogregenerates the catalog with no diff.Insert prompt paths literally (
workflow.tsx,renderAgentPrompt)The two
replaceAllcalls 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 thatprepareArtifactMirrorinstalls an enabled friction log. Before, deleting theprepareFrictionLog(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:--bodyvalue. 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.:logs and lists, and nothing lands in the target.packages/runtime/test/runtime.test.tsandpackages/runtime/test/trusted-cli.test.tsassert 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.Docs and CHANGELOG (
docs/reference/configuration.md,CHANGELOG.md)The configuration reference and the CHANGELOG entry now say:
docs/security.md), so<run>/frictionis where the command writes, not a boundary, and the command removes no GitHub credential.GIT_DIRset to a path that does not exist, never a barenpx frog. Without the fence,list --cwd <run>/frictionlists the target repository's log instead.logandlistwhile one entry is malformed. Deleting that entry's directory recovers.The CHANGELOG's #1201 entry gains one sentence. Frog's
postgressatisfies drizzle-orm's optionalpostgrespeer, which pnpm records in the dependency paths of the Smithers packages that reach drizzle-orm, the engine's included. So thepnpm installthat adds Frog moves the engine: let campaigns finish, or pause them, before that install, andultrafuzz resumea paused one afterwards.Review follow-ups on this branch
COMPLETEis unset, as described above.--updateamong them, failed no test.GIT_DIRfence in its review advice,GH_CONFIG_DIRwith the keyring reason, "where an option value belongs" (--formatis a built-in flag the command accepts as an option), and "the Smithers packages that reach drizzle-orm". Those are 25 of the 48smthrsand@smthrs/*lockfile snapshots: 24 gain a([email protected])suffix, and the hashed peer suffix of@smthrs/testingchanges.friction-log.ts, the configuration reference and the CHANGELOG said that<run>/frictionis where agents are told to write. They are told the opposite: to leave it to the command.Verification
All on this branch's head, from a clean build: every
distanddist-testdirectory and the generated dashboard sources removed, thenpnpm install --frozen-lockfile --prefer-offline(already up to date).pnpm -w buildpnpm -w typecheckpnpm -w lintCI=1 ESLINT_PLUGIN_DIFF_COMMIT=f13850e5 pnpm -w lint:strict:cipnpm -w knippnpm -w format:checkpnpm -w docs:checkpnpm -w docs:prompt-catalogdocs/reference/prompt-catalog.mdwith no diffeatmydata pnpm --filter @ultrafuzz/runtime test:release:supportingeatmydata node scripts/run-runtime-test-shard.mjs N/4for N = 1..4 inpackages/runtime, in parallel, on thedist-testthat run compilednpx vitest run --testTimeout=30000inpackages/promptstsc -p tsconfig.test.json && eatmydata node --test dist-test/test/e2e/*.test.jsinpackages/cli(test:e2ewithout its build step, which the clean build had done)Checked against the real frog 1.1.0 and incur 0.4.25 in scratch fixtures outside the repository:
GIT_CEILING_DIRECTORIESset to the run root) and a:in the path, Frog wrote the entry toa:b-target/.agents/friction-log/.log --cwd <run>/friction x --body <flag>),--mcpserved an MCP server whosesearch_toolsreturnedpublish,--updateranpnpm add --global frog@latest(a stand-inpnpmrecorded the call), and--llmsprinted Frog's manifest.COMPLETEwas unset, the command exited 0 withCOMPLETE=bash, printed nothing and wrote no entry.--body, Frog run directly opened$EDITORon the new entry; through the command it did not, andlogfailed withMISSING_BODY. With a terminal on stdout, Frog run directly wroteincur/updates/frog.jsonunderXDG_CACHE_HOME; through the command it wrote nothing.GH_CONFIG_DIR=<missing path> gh auth tokenexits 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 (ActiveTokenfalls back toTokenFromKeyring).list --cwd <run>/frictionwithoutGIT_DIRprintedentries: [], the target's empty log; withGIT_DIR=/nonexistentit listed the run's entries.listandlogfail withMALFORMED_ENTRY; deleting its directory recovered.Real engine: a smoke campaign on a small Foundry vault with
friction_log_enabled = true, on19459fc4(this branch before its review follow-ups, which change the wrapper only by also unsettingCOMPLETE), finishedsucceededwith a verified, complete report in 11m 33s. During it an agent recorded one entry through the command, under<run>/friction/.agents/friction-log/. Afterwards,listand an operatorlogworked;publish,log x --body --mcp,--body --llms,-s --schema,--label --help,--label -h,--cwd /tmpand a trailing--labelall exited 2; the target root had no.agents; and the run held no copy of Frog. The same campaign onmainbefore 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.
INCUR_BUILTIN_FLAGSfails the refusal test. Against the refusal table before this branch's review follow-ups, 12 of them passed.GIT_CEILING_DIRECTORIESinstead ofGIT_DIR;GIT_DIRnot exported;GIT_WORK_TREE, theFROG_*variables orCOMPLETEnot unset; noGH_CONFIG_DIR; noNO_UPDATE_NOTIFIER; stdin not/dev/null; option values not checked.frogedge followed in the trusted CLI closure (trusted-cli.test.ts) or in the execution snapshot (startRun installs, seals, and revalidates operator-owned Smithers).prepareFrictionLog(task)call removed fromprepareArtifactMirror; 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 fromaddDir.Measurements: the trusted CLI closure comes from
prepareTrustedCliEnvironmentonpackages/cli/dist/index.js, andrunTrustedJsonValidatorPreflightpasses on it. The execution snapshot comes from a scratchultrafuzz runwithSMITHERS_BINset to a stand-in Smithers. For the "before" rows, thefrogcondition was removed from a copy of the closure code and from the builtsmithers.js, which was restored afterwards.Not in this PR
run.frictionLogEnabledstays 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.ghor Frog directly. Making credentials unavailable to agents needs a sandbox, not this wrapper.cat entry.md | ... log) no longer works through the command, because Frog reads/dev/null. The prompt never uses it.INCUR_BUILTIN_FLAGSin step with incur.🤖 Generated with Claude Code
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.
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]Reviews (1) · Last reviewed commit: "docs: say the friction log command write..."