@@ -97,29 +97,65 @@ const VERIFICATION_SIGNAL = /test|check|lint|build|typecheck|verify/i;
9797const EVIDENCE_TOKEN = / \[ \[ e v i d e n c e : [ ^ [ \] \r \n ] + \] \] / g;
9898
9999// Standing reply-contract tokens (FILES_DONE=..., SUMMARY=...). The spine
100- // must keep these whole; a char slice is how they become FILES_DO.
101- const OUTPUT_FORMAT_TOKEN = / \b [ A - Z ] [ A - Z 0 - 9 _ ] + = \S + / g;
100+ // and the file goal must keep these whole; a char slice is how they become
101+ // FILES_DO. Harvest from the uncapped source so a 500-char cap cannot drop
102+ // a token that starts just past the cut.
103+ function outputFormatTokenRe ( ) : RegExp {
104+ return / \b [ A - Z ] [ A - Z 0 - 9 _ ] + = \S + / g;
105+ }
102106
103107function oneLine ( text : string , maxChars : number ) : string {
104108 return text . replace ( / \s + / g, " " ) . trim ( ) . slice ( 0 , maxChars ) ;
105109}
106110
107111function outputFormatTokens ( text : string ) : string [ ] {
108112 const found : string [ ] = [ ] ;
109- for ( const match of text . matchAll ( OUTPUT_FORMAT_TOKEN ) ) {
113+ for ( const match of text . matchAll ( outputFormatTokenRe ( ) ) ) {
110114 const token = match [ 0 ] ;
111115 if ( token !== undefined && ! found . includes ( token ) ) found . push ( token ) ;
112116 }
113117 return found ;
114118}
115119
120+ function capGoal ( text : string , maxChars : number ) : string {
121+ const line = text . replace ( / \s + / g, " " ) . trim ( ) ;
122+ const tokens = outputFormatTokens ( line ) ;
123+ let end = Math . min ( maxChars , line . length ) ;
124+ for ( const match of line . matchAll ( outputFormatTokenRe ( ) ) ) {
125+ const start = match . index ?? 0 ;
126+ const tokenEnd = start + match [ 0 ] . length ;
127+ if ( start < end && tokenEnd > end ) end = tokenEnd ;
128+ }
129+ let capped = line . slice ( 0 , end ) ;
130+ for ( const token of tokens ) {
131+ if ( ! capped . includes ( token ) ) capped = `${ capped } ${ token } ` ;
132+ }
133+ return capped ;
134+ }
135+
116136function cutSpineGoal ( text : string ) : string {
117137 const line = text . replace ( / \s + / g, " " ) . trim ( ) ;
118138 if ( line . length <= SPINE_GOAL_CHARS ) return line ;
119- const head = line . slice ( 0 , SPINE_GOAL_CHARS ) ;
139+ let cap = SPINE_GOAL_CHARS ;
140+ for ( const match of line . matchAll ( outputFormatTokenRe ( ) ) ) {
141+ const start = match . index ?? 0 ;
142+ const tokenEnd = start + match [ 0 ] . length ;
143+ if ( start < SPINE_GOAL_CHARS && tokenEnd > SPINE_GOAL_CHARS ) {
144+ if ( start === 0 ) {
145+ const kept = match [ 0 ] ;
146+ return kept . length < line . length
147+ ? `${ kept } ${ SPINE_CUT_SENTINEL } `
148+ : kept ;
149+ }
150+ cap = start ;
151+ break ;
152+ }
153+ }
154+ const head = line . slice ( 0 , cap ) ;
120155 const lastSpace = head . lastIndexOf ( " " ) ;
121- const kept = lastSpace > 0 ? head . slice ( 0 , lastSpace ) : "" ;
122- return kept . length > 0 ? `${ kept } ${ SPINE_CUT_SENTINEL } ` : SPINE_CUT_SENTINEL ;
156+ const kept = lastSpace > 0 ? head . slice ( 0 , lastSpace ) : head . trimEnd ( ) ;
157+ if ( kept . length === 0 ) return SPINE_CUT_SENTINEL ;
158+ return `${ kept } ${ SPINE_CUT_SENTINEL } ` ;
123159}
124160
125161function cutSpineItem ( text : string ) : string {
@@ -235,7 +271,9 @@ function parseSpineText(text: string): CarriedFacts {
235271 for ( const line of text . split ( "\n" ) ) {
236272 const trimmed = line . trim ( ) ;
237273 if ( trimmed . startsWith ( "Goal: " ) ) {
238- const goal = trimmed . slice ( "Goal: " . length ) . trim ( ) ;
274+ let goal = trimmed . slice ( "Goal: " . length ) . trim ( ) ;
275+ if ( goal . endsWith ( SPINE_CUT_SENTINEL ) )
276+ goal = goal . slice ( 0 , - SPINE_CUT_SENTINEL . length ) ;
239277 if ( goal . length > 0 ) carried . goal = goal ;
240278 } else if ( trimmed . startsWith ( HANDOFF_OUTPUT_LINE_PREFIX ) ) {
241279 for ( const token of outputFormatTokens (
@@ -524,14 +562,14 @@ export function extractHandoffArtifact(
524562 // instead of dropping it. The verify-repair appendix is stripped first: it
525563 // carries exact file lists that stay in the fat file by design (CL-8744) and
526564 // must not leak into the thin spine.
527- const narrativeGoal = oneLine (
565+ const narrativeGoal = capGoal (
528566 narrative . split ( VERIFY_REPAIR_HEADING ) [ 0 ] ?? "" ,
529567 MAX_GOAL_CHARS ,
530568 ) ;
531569 const extractedGoal =
532570 carried . goal ??
533571 ( nonEmptyUserTexts . length > 0
534- ? oneLine ( nonEmptyUserTexts [ 0 ] ?? "" , MAX_GOAL_CHARS )
572+ ? capGoal ( nonEmptyUserTexts [ 0 ] ?? "" , MAX_GOAL_CHARS )
535573 : narrativeGoal . length > 0
536574 ? narrativeGoal
537575 : "Unknown (no user message in folded turns)" ) ;
@@ -541,7 +579,7 @@ export function extractHandoffArtifact(
541579 const lastUserText = [ ...nonEmptyUserTexts ] . pop ( ) ;
542580 const lastUserAsNext =
543581 lastUserText !== undefined &&
544- oneLine ( lastUserText , MAX_GOAL_CHARS ) !== extractedGoal &&
582+ capGoal ( lastUserText , MAX_GOAL_CHARS ) !== extractedGoal &&
545583 oneLine ( lastUserText , SPINE_GOAL_CHARS ) !== carried . goal ;
546584
547585 const freshDecisions : string [ ] = [ ] ;
0 commit comments