1515import { collectRepoContext } from "./repo-context.js" ;
1616import type { RepoContextSnapshot , CanonicalCommand } from "./repo-context.js" ;
1717import { redactSecrets } from "./permission-impact.js" ;
18+ import { safeCutEnd } from "./text-cut.js" ;
1819
1920export const PLAN_SCHEMA = "oh-my-cli.plan" ;
2021export const PLAN_VERSION = 1 ;
@@ -60,7 +61,10 @@ export interface TaskPlanOptions {
6061}
6162
6263function redactObjective ( task : string ) : string {
63- return redactSecrets ( task ?? "" ) . text . trim ( ) . slice ( 0 , MAX_OBJECTIVE ) ;
64+ const trimmed = redactSecrets ( task ?? "" ) . text . trim ( ) ;
65+ // Issue #874: safeCutEnd drops an astral char straddling the bound whole
66+ // rather than orphaning a surrogate.
67+ return trimmed . slice ( 0 , safeCutEnd ( trimmed , MAX_OBJECTIVE ) ) ;
6468}
6569
6670// Derive the ordered verify commands from a repository-context snapshot, in
@@ -70,7 +74,12 @@ export function deriveVerifyCommands(snapshot: RepoContextSnapshot): string[] {
7074 const commands : string [ ] = [ ] ;
7175 for ( const key of CANONICAL_ORDER ) {
7276 const ref = snapshot . commands [ key ] ;
73- if ( ref ) commands . push ( redactSecrets ( ref . command ) . text . slice ( 0 , MAX_COMMAND_LEN ) ) ;
77+ if ( ref ) {
78+ const text = redactSecrets ( ref . command ) . text ;
79+ // Issue #874: safeCutEnd drops an astral char straddling the bound whole
80+ // rather than orphaning a surrogate.
81+ commands . push ( text . slice ( 0 , safeCutEnd ( text , MAX_COMMAND_LEN ) ) ) ;
82+ }
7483 if ( commands . length >= MAX_COMMANDS ) break ;
7584 }
7685 return commands ;
0 commit comments