W2.4 verification: route smoke + zeitwerk, advisory tier, persisted per-check results - #47
Merged
Merged
Conversation
VerifyRevision's five checks prove the lockfile is satisfied and the app boots; nothing in the set ever requests a page. `before_action :authenticate_user!` without devise (project 27, 2026-05-14) passed every check and surfaced only when the user opened the preview, because `rails test` catches it only if the agent happened to write a test for that action, and is skipped entirely when it wrote none. This adds the check without wiring it: CHECKS is untouched, so no runtime behaviour changes, but the check is exercisable against every existing workspace. - lib/roast/route_smoke_check.rb is a Minitest integration test copied into the workspace as tmp/hifumi/route_smoke_test.rb for one `bin/rails test` run and removed again (four April-era workspaces do not gitignore tmp/, and W2.5's `git add -A` would commit it). One test per static GET route: must not raise, must not 5xx, 10 s per request. Rails-mounted controllers (rails/, active_storage/, action_mailbox/, turbo/) are skipped — /rails/conductor/action_mailbox/inbound_emails 500s on every generated app. Exceptions are flunked with one line: tmp/ is not one of Rails' app dirs, so the backtrace cleaner falls back to the full ~70-frame trace otherwise (12k chars per failure, measured). - Routes listed in tmp/hifumi/known_failing are skipped and every failing path lands in tmp/hifumi/failing — the plumbing for a pre-agent baseline so only breakage new in a revision reaches the fix agent. - VerifyRevision gains an ADVISORY tier (route_smoke triggers remediation but never blocks the commit), blocking_failed?, run_one, a per-check HINTS line for the fix prompt (Minitest ends with a re-run hint for a file the verifier deletes), the check symbol and duration on every result, and Shellwords.escape on the workspace in run_cmd. - bin/verify-workspace runs the checks against a workspace outside a roast subprocess; `--check NAME` for one, `--known-failing PATH,PATH` for the baseline view. Dev only: it executes the workspace's code unsandboxed. Route smoke passes on all 29 existing workspaces at HEAD (project_27's working tree carries an untracked BCrypt fixture that fails every test at load); injected `authenticate_user!`, a known-failing skip and a `sleep 30` timeout behave as designed on a scratch copy of project_43. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
format_errors was the last unbounded prompt input in the revision workflow: the full stdout+stderr of every failing check went verbatim into agent(:fix), up to three times per revision. Measured on project_43, `rails test` with every action broken is 10 425 chars; 28k chars of stacktrace across three fix prompts is the shape of incident this closes. StatCap (60/50 lines), the 16 000-char diff slice in update_docs and AppState::DOC_FILE_CAP already bound theirs. Head-and-tail at 4 000 / 1 000 chars rather than head-only because Minitest puts its run/failure/error counts on the last line — the most useful single signal once the head is cut, and the same reason StatCap keeps the git-stat summary line. The marker between says how much was dropped; the agent can re-run the suite itself. Lands before the per-check results are persisted, so the stored error text is capped from its first row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
Nothing but exit codes crossed the roast subprocess boundary: which check
failed, and why, existed only in container logs — contrary to W2.F1 ("save
last verification errors") and W2.F3 in the architecture canon. Landing
this before the check set changes means the new checks' real failure rate
is measured from their first production run.
- VerifyRevision.sentinel emits one [HIFUMI:VERIFY] line of JSON after
every verification run — W2.4, W2.AR (with the recipes applied) and
W2.RV (with the attempt number, from the repeat index every cog input
block receives) — carrying per-check name, pass/fail, advisory flag,
duration, capped error text and, for route smoke, the failing paths.
- VerifyReport.parse_line (lib/roast, since both sides need the prefix)
validates the record's shape rather than the prefix alone: agent output
is arbitrary and the parser runs inside the job's log-streaming threads,
so anything malformed is nil, never a raise.
- ExecuteInstructionJob scans BOTH subprocess streams. Probed on roast-ai
1.1.0 with a ruby-only workflow: Roast captures whatever a cog prints
(STDOUT.puts included) and relays it through its own logger on stderr as
"I, [ts] INFO -- ruby(:verify) ❯ <line>", 25k-char lines intact, and
the bin/roast-* wrappers exec without redirection. So the prefix is
located anywhere in the line, and run_roast_subprocess returns a fourth
element — existing three-element stubs destructure to nil. Records land
in revision.metrics["verify"] in order, and the key is absent when the
subprocess printed none.
- cap_error scrubs invalid UTF-8: JSON.generate raises on it, and a
logging failure must not abort the verify cog.
E2E (E2E_GENERATE=1, 2026-09-06, 513 s): all three revisions carry a W2.4
record with the four live checks. The test asserts the round-trip itself
now — it is transactional, so its rows are gone afterwards.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
CHECKS = %i[bundle_check db_prepare zeitwerk_check route_smoke rails_test] boot_check out: `rails runner "puts :ok"` failed in exactly one row of the measured coverage matrix, and db:prepare — which boots the same app, first — failed there too. herb_lint out: guarded on a gem the skeleton never had, it returned nil on every revision of every project since the Phase 1 spike. Reviving it was evaluated and rejected: on four real generated apps every error-severity finding was a style convention (instance variables in partials, `<input />`, autocomplete) — 16 and 10 errors on two apps that render fine. zeitwerk_check in, blocking: eager-loads every app/ file, the only check that sees code no route and no test touches — a syntax error in an orphan model, a constant missing at class-body level, a filename/constant mismatch — all of which passed the old set. route_smoke in, advisory: requests every static GET page in the test env. Closes the project-27 class of bug (`before_action :authenticate_user!` without devise), which passed every old check and surfaced only when the user opened the preview. Advisory because one raising page among many is not a reason to throw away a revision and the ones queued behind it: it still enters W2.AR/W2.R with the exact exception, and if two fix attempts do not resolve it the revision commits with the failure recorded in the verify sentinel (W2.F0) instead of resetting. W2.B — a route smoke baseline at the parent commit, before the agent runs — is what makes advisory safe: W2.4 skips pages already failing at HEAD, so only breakage new in this revision reaches the fix agent, and a page committed broken is never re-remediated on every later revision. Verified: the unit suite; all 29 existing workspaces on the new set — zeitwerk 29/29, route smoke 29/29 at HEAD, the same three pre-existing rails-test failures as before the change and nothing new; the gated E2E green in 644 s with every revision recording W2.B then a five-check W2.4; and two deterministic dev runs on project 37: an untested page raising `authenticate_user!` gave W2.B clean → W2.4 route smoke FAIL (advisory, failing_routes ["/about"]) with every blocking check green → W2.RV #1 green → committed; the same page hand-committed broken at HEAD gave W2.B failing_routes ["/about"] → W2.4 all green with the page skipped → no remediation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
The canon described the pre-2026-05-01 shape: an a–e check list with herb lint and a boot check, no W2.AR, a failure path that promised to save verification errors nothing ever saved, and a chat agent said to hold the errors in context. Bring it to what the code does now. - 01-workflows-and-decisions.md: W2.B (route smoke baseline at the parent commit), the five checks with tier and the one-line reason each exists, W2.AR (present since 2026-05-01, never documented), W2.F0 (advisory-only failure commits), W2.F1/F3 and D6 stated as built — check-level detail lives in revision.metrics["verify"], the chat sees status only — and a "W2.4, as built" note recording why boot_check and herb_lint went and why herb was evaluated and rejected rather than revived. - 03-tech-stack.md: the Herb + ReActionView section and the two "why not" rows that pointed at it are gone; the intent left the canon with the decision recorded where decisions live. - 01-vision-and-principles.md, 02-user-journey.md: the three places that repeated the old check chain as current behaviour. - 05-followups.md: the "grep for missing-gem signals" entry is deleted — route smoke catches that class where it breaks, generically — and a dated section holds what this work left open, including the net-empty-revision W2.6 defect observed while verifying. - 01-git-integration.md: the hook idea names rubocop, which generated apps ship, instead of HERB. - CLAUDE.md: a W2.4 verification convention bullet — the five checks, blocking vs advisory, W2.B, the tmp/hifumi mechanics, the stderr-relayed sentinel, metrics["verify"], the error cap, bin/verify-workspace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
Brakeman flagged run_cmd's backticks (`cd #{Shellwords.escape(workspace)}
&& #{cmd} 2>&1`) as possible command injection. There was no path for one:
cmd is one of five literals from perform, workspace is the job's own
project.workspace_path, escaped. The warning appeared because the ignore
entry for the old text stopped matching when the line changed.
Instead of re-ignoring, remove the shell. Each check is an argv array
spawned with Open3.capture2e(*argv, chdir: workspace): nothing to escape,
no shell to inject into, arguments reach the program byte-for-byte, and
stderr merges into stdout as `2>&1` did. A program that cannot start
(missing bin/rails, missing workspace) is a failed check carrying the
Errno as its output, which is what the shell's "command not found" used
to be. The two verify_revision.rb entries in config/brakeman.ignore are
gone — one for the line that no longer interpolates, one for
gem_available?, deleted with herb_lint. AutoRemediate.shell keeps its
shell and its entry; its cmd comes from the RECIPES table alone.
Tests pin the property directly: `$HOME; rm -rf / && \`id\` | cat` arrives
at printf as a literal string.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
CI's bin/bundler-audit went red on the ruby-advisory-db update of 2026-09-05: rubyzip 3.3.0, path traversal, fixed in >= 3.4.0. rubyzip is a transitive test-group dependency via selenium-webdriver (>= 1.2.2, < 4.0), so `bundle update --conservative rubyzip` moves that one line and nothing else. Unrelated to the branch it lands on. The skeleton's own lockfile (lib/preview/skeleton/Gemfile.lock) pins rubyzip 3.2.2 through the same selenium dependency; CI does not audit it, and bumping it changes the bundle baked into the generator image and every new workspace, so it is left for its own change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd
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.
Summary
VerifyRevision's five checks proved two things about a generated app: the lockfile is satisfied and the app boots. Nothing ever requested a page,herb_lintnever ran (herb is not in the skeleton Gemfile), andboot_checkfailed only wheredb:preparehad already failed. This replaces the two dead checks with two that close measured coverage holes, caps the fix agent's error input, persists every verification run on the revision, and reconciles the docs.Plan and measurements:
thoughts/shared/plans/2026-09-05/verify-revision-coverage-rework.md(localthoughts/, not in the repo).Five commits, ordered so the behaviour change lands last with its safety net already in place, each independently revertable:
777aeb7bin/verify-workspace— unwired, no runtime change21259c8cc988f1a35ecf7c8b0fd6What changes for a revision
CHECKS = %i[bundle_check db_prepare zeitwerk_check route_smoke rails_test].zeitwerk:checkeager-loads everyapp/file (the only check that sees code no route and no test touches). Route smoke is a Minitest integration test copied into the workspace'stmp/hifumi/for onebin/rails testrun and removed again: every static GET page must not raise or 5xx, 10 s per request, Rails-mounted controllers skipped.revision.metrics["verify"]holds every verification run in order (W2.B, W2.4, W2.AR, W2.RV × 2) with per-check name, pass/fail, tier, duration, capped error text and failing paths. Transport is a[HIFUMI:VERIFY]JSON line on the roast subprocess's output, parsed byVerifyReportinExecuteInstructionJob. Closes W2.F1/W2.F3 of the canon, which promised this and never had it.ERROR_CAP_CHARS), keeping Minitest's counts line — the last unbounded prompt input in the workflow.bin/verify-workspace [--check NAME] [--known-failing PATH,PATH] <workspace>....Verification
zeitwerk check29/29, route smoke 29/29 at HEAD, notmp/hifumileftovers (four April-era workspaces do not gitignoretmp/). The onlyrails testfailures are three pre-existing ones (two dirty working trees, one date-rotted fixture set) — identical before and after.authenticate_user!→FAIL route smoke (advisory)with theNoMethodErrorline; orphan-model syntax error →FAIL zeitwerk checkalone;sleep 30→Timeout::Errorat 11 s.E2E_GENERATE=1) green twice: 513 s after commit 3 (every revision records a four-check W2.4), 644 s after commit 4 (every revision records W2.B then a five-check W2.4). The test now asserts the metrics round-trip itself, since its rows roll back./aboutpage raisingauthenticate_user!→ W2.B clean, W2.4 fails only route smoke (failing_routes: ["/about"]), W2.RV build(deps): bump actions/upload-artifact from 4 to 7 #1 green, committed (59 s). The same page hand-committed broken at HEAD → W2.Bfailing_routes: ["/about"], W2.4 all green with the page skipped, no remediation (47 s). Plus a deliberately failing test →metrics["verify"]namesrails testwith the Minitest failure text, then W2.RV green.E2E_PREVIEW=1) still green.Where the plan was wrong, and what was done instead
STDOUT.putsincluded) arrives asI, [ts] INFO -- ruby(:verify) ❯ <line>on stderr, 25k-char lines intact. SoVerifyReport.parse_linelocates the prefix anywhere in the line and the job scans both streams; the plan assumed stdout at column 0.cap_errorscrubs invalid UTF-8:JSON.generateraises on it, and a logging failure must not abort the verify cog.bin/verify-workspacegrew--known-failingand a per-check duration + Minitest counts tail, instead of the plan's throwaway Ruby driver, so the baseline view is reproducible from the repo.rails testfor reasons unrelated to verification (recorded in the plan). Both new checks are clean fleet-wide.Not done here
The
FORCE_AGENT_SANDBOX=1run: no local generator image, and the followups doc records the sandbox path cannot run on the macOS dev box. After deploy, one production row closes it:Expected: a
W2.Brecord first, thenW2.4with five checks androute smokemarkedadvisory: true.Follow-ups recorded (
docs/09-ideas/05-followups.md, 2026-09-07)Never discarding a committable state (build only if the "fix made it worse" query returns rows); a net-empty revision still runs W2.6 against the parent commit (pre-existing, observed here); telling the code agent about the smoke check in
RevisionPrompt;db:prepareininit_rails_app; hardcoded dates rotting generated test suites.🤖 Generated with Claude Code
https://claude.ai/code/session_01VkPCN5GsAR8p3gDDVPYZEd