diff --git a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java index 8d21ba2..202eab2 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java +++ b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java @@ -89,6 +89,9 @@ public final class CompletionResolver implements TurnListener { /** A pane echo must be this large before it can replace a completion report. */ static final int ECHO_MIN_CHARS = 400; + /** Normalised TUI chrome may add this many characters to an otherwise echoed brief. */ + static final int MAX_ECHO_EXCESS_CHARS = 160; + /** The explicit result returned instead of a lead's echoed injected brief. */ public static final String NO_REPORT_PREFIX = "[no report — the member ended its turn without fleet_reply, " + "and the pane still shows the injected brief. Nothing was produced on the pane. Check the " @@ -421,9 +424,9 @@ public final class CompletionResolver implements TurnListener { } /** - * A full echoed brief is at least 400 normalised characters and one normalised value contains the - * other. This accepts harmless TUI whitespace and punctuation changes, but preserves a real report - * that quotes only one part of the brief. + * A full echoed brief is at least 400 normalised characters. A scrape that contains the brief may + * add no more than 160 normalised characters of TUI chrome. This accepts harmless status text, but + * preserves a real report that restates the full brief before adding substantive content. */ static boolean echoesInjectedBrief(String scrape, String injectedText) { String normalScrape = normalize(scrape); @@ -431,7 +434,11 @@ public final class CompletionResolver implements TurnListener { if (normalScrape.length() < ECHO_MIN_CHARS || normalInjected.length() < ECHO_MIN_CHARS) { return false; } - return normalInjected.contains(normalScrape) || normalScrape.contains(normalInjected); + if (normalInjected.contains(normalScrape)) { + return true; + } + return normalScrape.contains(normalInjected) + && normalScrape.length() - normalInjected.length() <= MAX_ECHO_EXCESS_CHARS; } private static String normalize(String text) { diff --git a/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java b/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java index 36b4600..ad11a37 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java @@ -196,6 +196,26 @@ class CompletionResolverTest { assertEquals("complete report", waiter.getNow(null).text()); } + @Test + void suppressesABareEchoWithOnlyTuiChrome() { + String injected = "Load the implementer skill. You own fleetd #999. ".repeat(12); + String scrape = injected + "\nDev auto - GPT-5.6 Terra OpenAI"; + + assertTrue(CompletionResolver.echoesInjectedBrief(scrape, injected)); + } + + @Test + void pinsTheMaximumTuiChromeExcess() { + String injected = "a".repeat(CompletionResolver.ECHO_MIN_CHARS); + String underMargin = injected + "b".repeat(CompletionResolver.MAX_ECHO_EXCESS_CHARS); + String overMargin = injected + "b".repeat(CompletionResolver.MAX_ECHO_EXCESS_CHARS + 1); + + assertTrue(CompletionResolver.echoesInjectedBrief(underMargin, injected), + "the configured excess itself remains an echoed brief"); + assertFalse(CompletionResolver.echoesInjectedBrief(overMargin, injected), + "one character beyond the excess must preserve the scrape as a real report"); + } + @Test void resolvesSynchronouslyBeforePostTurnContextClearing() { FakeHerdr herdr = new FakeHerdr().readText("⏺ previous answer\n❯ "); diff --git a/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java b/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java index f62684c..7e7af3c 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/msg/MessageServiceTest.java @@ -109,11 +109,11 @@ class MessageServiceTest { } @Test - void completionFallbackKeepsARealReportThatQuotesPartOfTheBrief() throws Exception { - String quotedAcceptance = "Acceptance: the new test passes and the build is green. ".repeat(4); - String brief = quotedAcceptance + "Implement the requested change. ".repeat(20); - String report = "I changed the fallback and added tests. " + quotedAcceptance - + "The final build passed."; + void completionFallbackKeepsARealReportThatRestatesTheWholeBrief() throws Exception { + String brief = "Load the implementer skill. You own fleetd #999. ".repeat(12); + String report = brief + "\n\n## Report\n" + + ("I implemented the fix in GitWorktrees.java, added five tests, ran mvn clean install " + + "and got 1168 tests with 0 failures. Commit 321d8dc pushed. ").repeat(8); CompletableFuture send = sendAsync(brief); awaitWaiting(); @@ -125,7 +125,7 @@ class MessageServiceTest { MessageService.Reply reply = send.get(5, TimeUnit.SECONDS); assertEquals(MessageService.Outcome.COMPLETED_UNREPLIED, reply.outcome()); - assertEquals(report, reply.text(), "a report that quotes part of the brief must survive unchanged"); + assertEquals(report.strip(), reply.text(), "a report that restates the whole brief must survive unchanged"); } @Test