@@ -38,6 +38,8 @@ export const SKILL_NUDGE_OPT_OUT_ENV = 'TESTSPRITE_NO_SKILL_WARNING';
3838export interface SkillPresenceDeps {
3939 existsSync ?: ( p : string ) => boolean ;
4040 readFileSync ?: ( p : string ) => string ;
41+ /** Best-effort diagnostic hook for an unreadable managed-section target. */
42+ onReadError ?: ( path : string , error : unknown ) => void ;
4143}
4244
4345/**
@@ -60,7 +62,14 @@ export function isVerifySkillInstalled(dir: string, deps: SkillPresenceDeps = {}
6062 if ( spec . mode === 'managed-section' ) {
6163 try {
6264 if ( hasCompleteManagedSection ( read ( full ) ) ) return true ;
63- } catch {
65+ } catch ( error ) {
66+ // A diagnostic callback must not change this best-effort probe's
67+ // behavior, even if the caller's stderr sink itself is unavailable.
68+ try {
69+ deps . onReadError ?.( full , error ) ;
70+ } catch {
71+ // ignore diagnostic delivery failures
72+ }
6473 // unreadable AGENTS.md → treat this target as absent, keep checking
6574 }
6675 continue ;
@@ -92,6 +101,8 @@ export interface SkillNudgeContext {
92101 readProfileImpl ?: ( profile : string , opts : { path : string } ) => { apiKey ?: string } | undefined ;
93102 /** Sink for the hint line; defaults to `process.stderr`. */
94103 stderr ?: ( line : string ) => void ;
104+ /** Emit best-effort diagnostics for swallowed nudge errors. */
105+ debug ?: boolean ;
95106 existsSync ?: ( p : string ) => boolean ;
96107 readFileSync ?: ( p : string ) => string ;
97108}
@@ -106,9 +117,11 @@ export interface SkillNudgeContext {
106117 * not `--dry-run`, the command is in {@link SKILL_NUDGE_COMMANDS}, the opt-out
107118 * env is unset, the active profile has an api key (un-configured callers hit an
108119 * auth error that already points at setup), and the skill is not already
109- * installed. Never throws and never blocks the command — any error is swallowed.
120+ * installed. Never throws and never blocks the command — any error is swallowed;
121+ * `--debug` callers receive the swallowed reason on stderr.
110122 */
111123export function maybeEmitSkillNudge ( ctx : SkillNudgeContext ) : void {
124+ const write = ctx . stderr ?? ( ( line : string ) => process . stderr . write ( `${ line } \n` ) ) ;
112125 try {
113126 if ( ctx . output !== 'text' ) return ;
114127 if ( ctx . dryRun ) return ;
@@ -124,20 +137,38 @@ export function maybeEmitSkillNudge(ctx: SkillNudgeContext): void {
124137 isVerifySkillInstalled ( ctx . cwd , {
125138 existsSync : ctx . existsSync ,
126139 readFileSync : ctx . readFileSync ,
140+ onReadError : ctx . debug
141+ ? ( path , error ) =>
142+ emitDebug (
143+ write ,
144+ `skill nudge could not read ${ path } ; treating target as absent` ,
145+ error ,
146+ )
147+ : undefined ,
127148 } )
128149 ) {
129150 return ;
130151 }
131152
132- const write = ctx . stderr ?? ( ( line : string ) => process . stderr . write ( `${ line } \n` ) ) ;
133153 write (
134154 '[warn] No TestSprite verification skill is installed in this project — your coding ' +
135155 'agent will not verify its changes against TestSprite. Run `testsprite setup` (or ' +
136156 `\`testsprite agent install\`) to set it up. Silence: ${ SKILL_NUDGE_OPT_OUT_ENV } =1` ,
137157 ) ;
138- } catch {
158+ } catch ( error ) {
139159 // A nudge must never break, delay, or alter the exit status of a real
140160 // command. Swallow everything (missing creds file, fs races, etc.).
161+ if ( ctx . debug ) emitDebug ( write , 'skill nudge skipped' , error ) ;
162+ }
163+ }
164+
165+ /** Emit a diagnostic without letting the diagnostic path break the command. */
166+ function emitDebug ( write : ( line : string ) => void , context : string , error : unknown ) : void {
167+ try {
168+ const reason = error instanceof Error ? error . message : String ( error ) ;
169+ write ( `[debug] ${ context } : ${ reason } ` ) ;
170+ } catch {
171+ // A broken stderr sink must not turn a best-effort nudge into a failure.
141172 }
142173}
143174
0 commit comments