diff --git a/fleetd/src/main/java/dev/ltms/fleet/lead/LeadRollover.java b/fleetd/src/main/java/dev/ltms/fleet/lead/LeadRollover.java
index f43c3c8..29cbb0c 100644
--- a/fleetd/src/main/java/dev/ltms/fleet/lead/LeadRollover.java
+++ b/fleetd/src/main/java/dev/ltms/fleet/lead/LeadRollover.java
@@ -270,24 +270,42 @@ public final class LeadRollover {
* FleetConfig.LeadRollover#bootstrapTextFor}) ever has to resolve — or worse, silently
* mis-resolve — a relative path again.
*
+ *
The return value is GUARANTEED absolute, not merely usually absolute.
+ * {@code leadWorkspace.apply(leadTerminal)} returns an operator-configured {@code
+ * fleet.leaders..cwd} string, and nothing forces an operator to write an absolute one —
+ * a relative {@code cwd} resolved with plain {@link Path#resolve} would still yield a relative
+ * result, silently reopening the exact bug this class exists to fix (every later reader back to
+ * interpreting an ambiguous string against ITS OWN working directory). {@link
+ * Path#toAbsolutePath()} closes that: it resolves any remaining relative path against {@code
+ * user.dir} (the JVM's own cwd), which is the correct base for an operator-written path the
+ * daemon process itself is meant to interpret, exactly like the {@code user.dir} fallback used
+ * below. Applying it unconditionally on both branches means the ALREADY-absolute branch stays a
+ * no-op (an absolute path is unaffected by {@code toAbsolutePath()}) while the relative-{@code
+ * cwd} branch above is closed the same way.
+ *
*
* - already absolute → returned unchanged (normalized)
* - relative → resolved against {@code leadWorkspace.apply(leadTerminal)} when that is
* non-null and non-blank; otherwise against {@code System.getProperty("user.dir")} — the
* same fallback {@code LeadLauncher#launch} uses for a lead with no configured {@code
- * cwd}
+ * cwd}. If {@code leadWorkspace}'s own answer is itself relative (an operator wrote a
+ * relative {@code cwd:}), the result is finished off against the daemon's own
+ * {@code user.dir} — see the paragraph above.
*
*/
private String resolveHandoverPath(String configured, String leadTerminal) {
Path path = Path.of(configured);
if (path.isAbsolute()) {
- return path.normalize().toString();
+ // toAbsolutePath() is a no-op for an already-absolute path — kept here anyway so both
+ // branches call the exact same guarantee, rather than one branch relying on
+ // isAbsolute() alone to already imply what toAbsolutePath() enforces.
+ return path.toAbsolutePath().normalize().toString();
}
String workspace = leadWorkspace.apply(leadTerminal);
Path base = (workspace == null || workspace.isBlank())
? Path.of(System.getProperty("user.dir"))
: Path.of(workspace);
- return base.resolve(path).normalize().toString();
+ return base.resolve(path).toAbsolutePath().normalize().toString();
}
/**
diff --git a/fleetd/src/test/java/dev/ltms/fleet/lead/LeadRolloverTest.java b/fleetd/src/test/java/dev/ltms/fleet/lead/LeadRolloverTest.java
index d2f56ff..8d69d92 100644
--- a/fleetd/src/test/java/dev/ltms/fleet/lead/LeadRolloverTest.java
+++ b/fleetd/src/test/java/dev/ltms/fleet/lead/LeadRolloverTest.java
@@ -575,6 +575,36 @@ class LeadRolloverTest {
"an absolute handoverPath must be used unchanged (aside from normalization)");
}
+ @Test
+ @DisplayName("[fleetd #480 follow-up correction] a RELATIVE fleet.leaders..cwd still "
+ + "yields an ABSOLUTE PendingRollover.handoverPath")
+ void relativeLeadWorkspaceCwdStillYieldsAnAbsoluteHandoverPath() {
+ FakeHerdr herdr = new FakeHerdr();
+ AtomicLong clock = new AtomicLong(1_000);
+ FleetConfig.LeadRollover config = cfg("handover.md"); // relative handoverPath
+ // Nothing in FleetConfig validates fleet.leaders..cwd, so an operator can write a
+ // RELATIVE one — this must still resolve to an absolute PendingRollover.handoverPath,
+ // never silently reopen the exact bug this class fixes.
+ String relativeWorkspace = "relative-lead-workspace";
+ Function leadWorkspace = terminal -> LEAD.equals(terminal) ? relativeWorkspace : null;
+ LeadRollover rollover = newRollover(herdr, config, fixedClock(clock), leadWorkspace);
+
+ LeadRollover.PendingRollover pending = rollover.open(LEAD, "context is full");
+
+ assertTrue(Path.of(pending.handoverPath()).isAbsolute(),
+ "the resolved path must be absolute even when the configured cwd itself is relative");
+
+ // Asserting isAbsolute() alone would also pass for a path resolved against the WRONG base
+ // (e.g. some unrelated absolute directory) — pin the actual value too.
+ String expected = Path.of(System.getProperty("user.dir")).resolve(relativeWorkspace)
+ .resolve("handover.md").toAbsolutePath().normalize().toString();
+ assertEquals(expected, pending.handoverPath(),
+ "a relative cwd must be finished off against the daemon's own user.dir, exactly "
+ + "like a missing cwd — never left relative, which would silently reopen the "
+ + "exact bug this class fixes: every later reader interpreting an ambiguous "
+ + "path against ITS OWN working directory");
+ }
+
@Test
@DisplayName("[fleetd #480 follow-up] a terminal with no configured workspace (null lookup "
+ "result) falls back to the daemon's own user.dir")