@@ -436,6 +436,27 @@ export type ForcedStopReason =
436436 | "repetition"
437437 | "incomplete-report" ;
438438
439+ // Exact Summary text for each forced-stop reason. This is the single source
440+ // of truth for both forcedStopReport (the producer) and the isXxxSubAgentReport
441+ // classifiers (the consumers) — CL-6704: classifying on a free-text substring
442+ // like "no progress" or "cancelled" hard-blocks a SUCCESSFUL report whose
443+ // Summary happens to contain that phrase. Matching the exact string a forced
444+ // stop actually produces closes that false-positive path without a report
445+ // schema change (a typed marker would need one; see CL-6786, out of scope).
446+ const FORCED_STOP_SUMMARIES : Record < ForcedStopReason , string > = {
447+ "no-progress" : "Stopped: repeated the same tool calls with no progress." ,
448+ "no-ship" : "Stopped: implement intent searched many files without writing any." ,
449+ "never-acted" : "Stopped: completed without using any tools." ,
450+ "never-edited" : "Stopped: implement intent finished without writing any files." ,
451+ cancelled : "Stopped: cancelled by operator before finishing." ,
452+ deadline : "Stopped: wall-clock deadline reached before finishing." ,
453+ stalled :
454+ "Stopped after a long silence with no tool activity. The parent can re-dispatch or check the background work directly." ,
455+ repetition : "Stopped: degenerate repetition in streamed output (same window looping mid-turn)." ,
456+ "incomplete-report" : "Stopped: worker narrated instead of writing a report envelope." ,
457+ "turn-budget" : "Turn budget reached before finishing." ,
458+ } ;
459+
439460/**
440461 * Build the parent-facing report when a leaf is force-stopped. There is no
441462 * further inference, so this must already be a full envelope — not an
@@ -449,26 +470,7 @@ export function forcedStopReport(
449470 partialText : string ,
450471 detail ?: string ,
451472) : string {
452- const summary =
453- reason === "no-progress"
454- ? "Stopped: repeated the same tool calls with no progress."
455- : reason === "no-ship"
456- ? "Stopped: implement intent searched many files without writing any."
457- : reason === "never-acted"
458- ? "Stopped: completed without using any tools."
459- : reason === "never-edited"
460- ? "Stopped: implement intent finished without writing any files."
461- : reason === "cancelled"
462- ? "Stopped: cancelled by operator before finishing."
463- : reason === "deadline"
464- ? "Stopped: wall-clock deadline reached before finishing."
465- : reason === "stalled"
466- ? "Stopped after a long silence with no tool activity. The parent can re-dispatch or check the background work directly."
467- : reason === "repetition"
468- ? "Stopped: degenerate repetition in streamed output (same window looping mid-turn)."
469- : reason === "incomplete-report"
470- ? "Stopped: worker narrated instead of writing a report envelope."
471- : "Turn budget reached before finishing." ;
473+ const summary = FORCED_STOP_SUMMARIES [ reason ] ;
472474 const blockers =
473475 reason === "no-progress"
474476 ? "Identical tool-call fingerprint repeated consecutively; parent must not re-dispatch the identical brief (it will be refused) — tighten success_criteria/do_not or change approach."
@@ -506,40 +508,40 @@ export function forcedStopReport(
506508 } ) ;
507509}
508510
511+ /**
512+ * True when a report's Summary is exactly the forced-stop text for `reason`
513+ * (CL-6704: exact match, not a free-text substring — a successful report
514+ * whose Summary happens to mention the same words must not classify as a
515+ * forced stop).
516+ */
517+ export function isForcedStopSubAgentReport ( report : string , reason : ForcedStopReason ) : boolean {
518+ const parsed = parseSubAgentReport ( report ) ;
519+ return parsed . summary === FORCED_STOP_SUMMARIES [ reason ] ;
520+ }
521+
509522/** True when the worker returned a turn-budget salvage report for the parent. */
510523export function isTurnBudgetSubAgentReport ( report : string ) : boolean {
511- const parsed = parseSubAgentReport ( report ) ;
512- return parsed . summary . includes ( "Turn budget reached" ) ;
524+ return isForcedStopSubAgentReport ( report , "turn-budget" ) ;
513525}
514526
515527/** True when the worker returned a never-acted salvage report for the parent. */
516528export function isNeverActedSubAgentReport ( report : string ) : boolean {
517- const parsed = parseSubAgentReport ( report ) ;
518- return parsed . summary . includes ( "without using any tools" ) ;
529+ return isForcedStopSubAgentReport ( report , "never-acted" ) ;
519530}
520531
521532/** True when implement intent finished without any write/edit tools. */
522533export function isNeverEditedSubAgentReport ( report : string ) : boolean {
523- const parsed = parseSubAgentReport ( report ) ;
524- return parsed . summary . includes ( "without writing any files" ) ;
534+ return isForcedStopSubAgentReport ( report , "never-edited" ) ;
525535}
526536
527537/** True when the worker returned a deadline salvage report for the parent. */
528538export function isDeadlineSubAgentReport ( report : string ) : boolean {
529- const parsed = parseSubAgentReport ( report ) ;
530- return parsed . summary . includes ( "deadline reached" ) ;
531- }
532-
533- /** True when the worker returned a progressive-thrash salvage report. */
534- export function isThrashSubAgentReport ( report : string ) : boolean {
535- const parsed = parseSubAgentReport ( report ) ;
536- return parsed . summary . includes ( "progressive thrash" ) ;
539+ return isForcedStopSubAgentReport ( report , "deadline" ) ;
537540}
538541
539542/** True when the worker returned a streamed-repetition salvage report. */
540543export function isRepetitionSubAgentReport ( report : string ) : boolean {
541- const parsed = parseSubAgentReport ( report ) ;
542- return parsed . summary . includes ( "degenerate repetition" ) ;
544+ return isForcedStopSubAgentReport ( report , "repetition" ) ;
543545}
544546
545547const TURN_BUDGET_PARENT_HINT =
@@ -558,9 +560,6 @@ const NEVER_EDITED_PARENT_HINT =
558560const DEADLINE_PARENT_HINT =
559561 "[Sub-agent hit an explicit wall-clock deadline before finishing. Continue from Findings rather than redoing completed work; re-dispatch with continuation context and a longer deadline only if more wall-clock time is warranted.]" ;
560562
561- const THRASH_PARENT_HINT =
562- "[Sub-agent stopped for progressive thrash (re-read pressure). Do not re-dispatch the identical brief (it will be refused) — change scope, success_criteria, and do_not; continue from Findings.]" ;
563-
564563const NO_SHIP_PARENT_HINT =
565564 "[Sub-agent stopped after searching many files without writing any. Do not search the repo yourself and do not re-dispatch the identical brief (it will be refused) — change success_criteria and do_not, or treat findings as unexecuted.]" ;
566565
@@ -611,15 +610,9 @@ export function appendDeadlineParentHint(report: string): string {
611610 return `${ DEADLINE_PARENT_HINT } \n\n${ report } ` ;
612611}
613612
614- export function appendThrashParentHint ( report : string ) : string {
615- if ( ! isThrashSubAgentReport ( report ) ) return report ;
616- return `${ THRASH_PARENT_HINT } \n\n${ report } ` ;
617- }
618-
619613/** True when the worker returned a no-ship (search-tour) salvage report. */
620614export function isNoShipSubAgentReport ( report : string ) : boolean {
621- const parsed = parseSubAgentReport ( report ) ;
622- return parsed . summary . includes ( "searched many files without writing" ) ;
615+ return isForcedStopSubAgentReport ( report , "no-ship" ) ;
623616}
624617
625618export function appendNoShipParentHint ( report : string ) : string {
@@ -634,16 +627,15 @@ export function appendRepetitionParentHint(report: string): string {
634627
635628/** True when the worker returned a no-progress salvage report. */
636629export function isNoProgressSubAgentReport ( report : string ) : boolean {
637- const parsed = parseSubAgentReport ( report ) ;
638- return parsed . summary . includes ( "no progress" ) ;
630+ return isForcedStopSubAgentReport ( report , "no-progress" ) ;
639631}
640632
641633export function appendNoProgressParentHint ( report : string ) : string {
642634 if ( ! isNoProgressSubAgentReport ( report ) ) return report ;
643635 return `${ NO_PROGRESS_PARENT_HINT } \n\n${ report } ` ;
644636}
645637
646- /** Stack parent-visible salvage hints for thrash / budget / never-acted / deadline / repetition / no-progress. */
638+ /** Stack parent-visible salvage hints for budget / never-acted / deadline / repetition / no-progress. */
647639export function appendSubAgentParentHints (
648640 report : string ,
649641 options : SubAgentParentHintOptions = { } ,
@@ -652,9 +644,7 @@ export function appendSubAgentParentHints(
652644 appendNeverEditedParentHint (
653645 appendNeverActedParentHint (
654646 appendTurnBudgetParentHint (
655- appendNoProgressParentHint (
656- appendNoShipParentHint ( appendThrashParentHint ( appendRepetitionParentHint ( report ) ) ) ,
657- ) ,
647+ appendNoProgressParentHint ( appendNoShipParentHint ( appendRepetitionParentHint ( report ) ) ) ,
658648 options ,
659649 ) ,
660650 ) ,
0 commit comments