ID |
|
|---|---|
Status |
Spec |
Bucket |
dx |
Priority |
4 |
Theme |
tooling |
Created |
2026-08-10 |
Updated |
2026-08-11 |
Redirect Maven output to a log file in web sessions via .mvn/maven.config
A full reactor build emits far more output than an agent session can hold, so the
harness truncates it. When a build fails, the failure detail an agent needs (the
surefire assertion, the compiler error, the module that broke) is frequently in the
part that got cut, and the only recovery is to re-run the build, paying the wall
clock a second time. Nothing currently preserves the untruncated output: it exists
only in the tool result. The warm build is the one exception, and only because the
SessionStart hook redirects it to /tmp/graphitron-web-env.log; every foreground
mvn / mvnd invocation loses its output the moment the tool result is trimmed.
An agent can add | tee by hand, but that only helps on the runs where it thought
to do so in advance, which is never the run that surprises it.
Maven already solves this. -l/--log-file is a stock flag on both mvn and mvnd,
and .mvn/maven.config makes it always-on with no wrapper, no PATH surgery, and
no change to what the build does. This item is only about switching that on in web
sandboxes and documenting the consequences.
Approach
Add one idempotent step to .claude/scripts/session-start-web-env.sh that writes
.mvn/maven.config at the repo root containing -l <fixed path under /tmp>
(/tmp/graphitron-maven.log, alongside the existing /tmp/graphitron-web-env.log).
Web sessions only, like every other step in that hook. .mvn/ is already gitignored,
so the file is sandbox-local and cannot reach CI or a teammate’s checkout.
The log is a single fixed path, truncated per run: the last build’s output, not a
history. Deliberate, and the reason the file lives under /tmp rather than target/:
mvn clean deletes the reactor root’s target/ at the start of the build and would
unlink a log opened there, leaving Maven writing to an inode nothing can reach.
(Verified empirically, not assumed.)
The warm build shares the same log. Install the config with the other config steps,
before the warm build, so the session’s longest and most informative build lands in
the same place every later build does and there is only ever one path meaning "the
last Maven build". Nothing has to be sequenced around it: the PreToolUse guard already
holds foreground mvn / mvnd until the warm build finishes, so the two can never
write the file concurrently.
The one case that needs handling is a failed warm build, whose output the first
foreground command would truncate away, and that first command is often a narrow
-pl that will not reproduce the failure. On the failure path only, copy the log
aside (/tmp/graphitron-warm-build-failure.log) before the hook exits, and repoint
the existing "Warm build FAILED … Log: $LOG_FILE" message at the copy, since
$LOG_FILE will no longer hold the Maven detail.
Then document the behaviour in .claude/web-environment.md and CLAUDE.md.
Consequences to document
-l is a redirect, not a tee: with it set, Maven prints nothing to the console.
That is the point (a green full build costs roughly zero tokens of tool output) but
it inverts the usual read, so the docs have to be explicit:
-
A build’s result is read from the exit code, then from the log. Silence is not success on its own, though a non-zero exit still surfaces normally, so a failed build is never invisible.
-
Commands whose stdout you actually need (
roadmap-tool exec:javaprinting what it wrote,help:evaluate, anything scripted against Maven output) must opt out. A command-line-loverrides the one inmaven.config, so-l /dev/stdoutrestores console output for a single invocation. Both facts verified. -
This subsumes the existing "don’t combine
mvndwith-qwhen you need stdout" quirk inweb-environment.mdunder a more general rule; that note should be rewritten rather than left to contradict this. -
web-environment.mdcurrently says totail -f /tmp/graphitron-web-env.logto watch warm-up progress. That file keeps the prerequisite step messages and the warm-build start/finish lines but no longer the Maven detail, so the progress-watching instruction has to move to the maven log. -
Two concurrent Maven invocations would overwrite each other’s log. The PreToolUse guard already serializes foreground Maven against the warm build, and agents issue builds one at a time, so this is a note and not a mechanism.
Reviewer findings (Spec → Ready gate, 2026-08-11)
Independent reviewer session, status stays Spec. Everything below was checked in a
web sandbox against Maven 3.9.11 (/opt/maven) and mvnd 1.0.6 (embedded Maven 3.9.16).
The mechanism does not hold up as specced, on three counts.
-
A repo-root
.mvn/maven.configcarrying-lbreaks the invoker integration tests, somvn installfails in every web session. Maven searches upward from the working directory for.mvn, and maven-invoker-plugin clones its projects tographitron-maven-plugin/target/it/<name>, inside the repo, then forks a realmvnthere. Each forked IT therefore inherits-land writes its Maven output to the shared log instead of the stdout the invoker captures intobuild.log.dependency-version-lag/verify.groovyandmissing-schema-inputs/verify.groovyassert onbuild.logcontent, so both fail: with the config installed,mvn -pl :graphitron-maven-plugin verify -Plocal-dbexits 1 withPassed: 1, Failed: 2andExpression: buildLog.contains(jooq-version-lag); with the config removed, the same command reportsPassed: 3, Failed: 0.basic-generateis the quieter half of the same problem: its assertions are negative (assert !buildLog.contains(...)), so an emptybuild.logsatisfies them vacuously and that IT’s coverage is silently voided rather than failing. -
mvnd honours
-lonly from the command line, not frommaven.config. With-linmaven.config,mvndcreates the log file, leaves it at 0 bytes, and prints the whole build to the console; all three argument spellings behave identically, and there is nomvnd.propertiesknob for a build log.-lon the mvnd command line does redirect. So "stock on bothmvnandmvnd`" is true of the flag but not of the always-on mechanism, and mvnd is both the tool `CLAUDE.mdtells agents to prefer and the one the warm build uses when present. As written, the item would deliver nothing on the recommended path while the docs promise the last build is in the log; the warm build would never share that log, and the copy-aside-on-failure step would copy an empty file. This is the premise the wrapper design was dropped on, so that fork is worth reopening: aPATHshadow puts-l(or atee) on the actual command line, which is the form mvnd respects, and is not inherited by the invoker’s forked${maven.home}/bin/mvn, so it sidesteps finding 1 as well. -
.mvn/maven.configis line-based, and the natural spelling silently does nothing.-l /tmp/graphitron-maven.logon a single line produces no log file, no warning, full console output, and exit 0.-land the path on separate lines,-l/tmp/..., and--log-file=/tmp/...all work. Whichever mechanism survives, pin the exact file bytes in the plan rather than describing them.
Two smaller points for the author, neither blocking:
-
The gate wants naming. "Web sessions only, like every other step in that hook" reads as precedent, but steps 1 to 5 are gated on sandbox markers rather than on web-ness; only the warm build tests
$ASYNC/CLAUDE_CODE_REMOTE. This step has no natural marker, and its local-dev failure mode is a contributor’s console silently going dark in their own checkout, so the plan should name the$ASYNCgate outright. -
Getting-started recommends consumers add
.mvn/jvm.config, so whatever writes the file shouldmkdir -pand write that one file, never replace the.mvndirectory.
The rest of the plan checks out against the tree: LOG_FILE=/tmp/graphitron-web-env.log
and the "Warm build FAILED … Log: $LOG_FILE" message are where the plan says they are,
.mvn/ is gitignored, web-environment.md does carry both the tail -f instruction and
the mvnd -q quirk the plan proposes to rewrite, and a command-line -l /dev/stdout does
override maven.config under mvn.
Second reviewer pass (Spec → Ready gate, 2026-08-11)
A second independent reviewer session, status stays Spec. The block above was written
by the previous gate pass and left the ## Approach section standing unchanged, so the
plan currently prescribes a mechanism its own findings section says does not work. That
is what blocks sign-off: there is nothing here an implementer could build without first
picking between the forks the previous pass opened, and that choice is the author’s.
All three findings reproduced independently, on the same toolchain (Maven 3.9.11 at
/opt/maven, mvnd 1.0.6 embedding Maven 3.9.16), so they are confirmed rather than
one session’s report and should not be re-litigated on the next pass:
-
Single-line
-l /tmp/...inmaven.config: no log file, full console output, exit 0. The two-line spelling redirects correctly undermvn(log written, console reduced to the JVM preamble). -
mvndwith that same working two-line config: creates the log file, leaves it at 0 bytes, prints the entire build to the console.-lon the mvnd command line does redirect (log written, console empty), which is what keeps the wrapper fork viable. -
Upward
.mvnsearch: a project under<root>/target/it/nestedpicks up the root’smaven.configand goes fully silent. Combined with the invoker’s<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>ingraphitron-maven-plugin/pom.xml, that is the whole mechanism behind finding 1, and the invoker forks${maven.home}/bin/mvnrather than resolvingmvnoffPATH, so aPATHshadow genuinely does sidestep it.
Three things to fold in when the Approach is rewritten:
-
The warm build’s Maven detail already lands in
/tmp/graphitron-web-env.logtoday, because the hook doesexec >>"$LOG_FILE" 2>&1before running it. Under the proposed mechanism with mvnd present (and the warm build prefers mvnd) it would keep landing there, since mvnd prints to a console that is already redirected. So "the warm build shares the same log" is false on the default path, and the two proposed doc edits invert: moving thetail -fprogress-watching instruction to the maven log would point agents at an empty file, and the copy-aside-on-failure step would copy one. -
The
## Explicitly out of scoperationale collapses with the mechanism. "The redirect is free and this item takes the free thing" is the entire stated reason for excluding a wrapper, and the redirect is not free: it costs two invoker ITs, silently voids a third’s negative assertions, and delivers nothing under mvnd. That section needs rewriting alongside the Approach, andteeis a live option to weigh rather than settled exclusion. -
Worth separating the two benefits explicitly, because the surviving mechanism changes which one you get. Recovering untruncated output after a surprise failure is the stated motivation; a green build costing near-zero tool output is a second, independent win that only a pure redirect buys. A
teewrapper gets the first and not the second.
One correction to the previous pass: getting-started does not recommend
.mvn/jvm.config, because no getting-started doc exists. The only live references are
roadmap/changelog.md’s R409 entry, which claims the section, and a Backlog item filed
to fix exactly that dangling pointer. The underlying advice still holds on other grounds
(.mvn/` is gitignored, so a contributor may have their own jvm.config there), so keep
the mkdir -p and write-one-file instruction, but do not attribute it to that doc.
Explicitly out of scope
Copying or archiving logs into target/, per-run log history, and any tee-style
wrapper that keeps console output live while also writing the file. Maven has no
built-in tee, so that would mean a PATH-shadowing wrapper script; the redirect is
free and this item takes the free thing.
(Both reviewer passes above bear on this section; see the note on the collapsed rationale before treating the exclusion as still standing.)