diff --git a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java index 652b28d..e1ff6e6 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java +++ b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java @@ -495,7 +495,7 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { *

Additive, not a rewrite. {@code .claude.json} is large (tens of KB, dozens of * projects) and Claude Code itself rewrites it while running, so this reads the file as a JSON * tree (missing or unreadable → treated as an empty object) and changes only - * {@code projects..hasTrustDialogAccepted} / {@code .hasCompletedProjectOnboarding} — + * {@code projects..hasTrustDialogAccepted} (that key alone — see fleetd #247) — * every other top-level key and every other project entry is written back untouched. Only the * one project entry for {@code cwd} is replaced/created; an existing entry for a DIFFERENT cwd * (or the operator's own project history) is never touched. @@ -558,8 +558,18 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { if (!(projectNode instanceof ObjectNode)) { projects.set(cwd, project); } + // fleetd #247: ONLY hasTrustDialogAccepted. We used to write + // hasCompletedProjectOnboarding beside it; do not put it back. Measured on + // 2026-09-03, minutes after a live spawn seeded this file: 28 of 28 project + // entries carried hasTrustDialogAccepted and 0 of 28 carried the onboarding key + // — including the 27 entries Claude Code wrote for itself. Claude Code + // normalises the whole file when it saves and drops that key every time, so + // writing it achieved nothing except making the next reader think it mattered. + // The member reached idle with the trust flag alone, which is the only outcome + // this seed exists for. If a future Claude Code needs the second flag the + // symptom returns as the trust dialog fleetd #149 describes — re-measure then, + // do not restore it on a guess. project.put("hasTrustDialogAccepted", true); - project.put("hasCompletedProjectOnboarding", true); writeAtomically(target, TRUST_JSON.writerWithDefaultPrettyPrinter().writeValueAsString(root)); } catch (Exception e) { log.debug("cannot seed workspace-trust entry for cwd '{}' into '{}'", cwd, target, e); diff --git a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java index ea5f991..d995775 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java @@ -2159,8 +2159,7 @@ class ClaudeCodeLauncherTest { } JsonNode root = new ObjectMapper().readTree(claudeJson.toFile()); JsonNode project = root.path("projects").path(worktree.toString()); - seededBeforeStart.set(project.path("hasTrustDialogAccepted").asBoolean(false) - && project.path("hasCompletedProjectOnboarding").asBoolean(false)); + seededBeforeStart.set(project.path("hasTrustDialogAccepted").asBoolean(false)); } catch (IOException e) { seededBeforeStart.set(false); } @@ -2178,7 +2177,7 @@ class ClaudeCodeLauncherTest { } @Test - void seedTrustDialogWritesBothTrustFlagsForTheResolvedCwd( + void seedTrustDialogWritesOnlyTheTrustFlagAndNotTheOnboardingKey( @TempDir Path configDir, @TempDir Path worktree) throws Exception { markAsProvisionedWorktree(worktree); FakeHerdr herdr = new FakeHerdr(); @@ -2192,7 +2191,13 @@ class ClaudeCodeLauncherTest { JsonNode project = new ObjectMapper().readTree(claudeJson.toFile()) .path("projects").path(worktree.toString()); assertTrue(project.path("hasTrustDialogAccepted").asBoolean(false)); - assertTrue(project.path("hasCompletedProjectOnboarding").asBoolean(false)); + // fleetd #247: the onboarding key must NOT be written. Claude Code strips it on every + // save (measured: 0 of 28 live entries had it, including its own), so writing it only + // adds a contested key to a file two processes share. This assertion is the guard that + // stops it coming back as a plausible-looking "completeness" fix. + assertFalse(project.has("hasCompletedProjectOnboarding"), + "hasCompletedProjectOnboarding must not be written — Claude Code drops it on " + + "every save, and the member reaches idle on hasTrustDialogAccepted alone"); } /** @@ -2238,7 +2243,6 @@ class ClaudeCodeLauncherTest { JsonNode mine = root.path("projects").path(worktree.toString()); assertTrue(mine.path("hasTrustDialogAccepted").asBoolean(false)); - assertTrue(mine.path("hasCompletedProjectOnboarding").asBoolean(false)); } /**