@@ -47,7 +47,13 @@ import { createInferenceDependencies } from "../provider/inference-dependencies.
4747import { getValidCodexToken } from "../auth/codex/session.js" ;
4848import { getValidXaiToken } from "../auth/xai/session.js" ;
4949import { refreshCodexInstructions } from "../auth/codex/instructions.js" ;
50- import { expandExistingPluginMembers , expandPluginPath , loadPluginEntry , type PluginOrigin } from "../plugins/loader.js" ;
50+ import {
51+ expandExistingPluginMembers ,
52+ expandPluginPath ,
53+ loadPluginEntry ,
54+ type ExpandPluginPathSkip ,
55+ type PluginOrigin ,
56+ } from "../plugins/loader.js" ;
5157import {
5258 createPluginLoadDiagnostics ,
5359 emitPluginWarningLog ,
@@ -411,6 +417,14 @@ export async function runTUI(initialConfig: Config): Promise<number> {
411417 diagnostics : pluginLoadDiag ,
412418 } ) ;
413419 emitPluginWarningLog ( pluginLoadDiag ) ;
420+ // Fire-and-forget startup diagnostics (this + tool-plugin resolution below)
421+ // have no result channel back to an operator action, unlike verify/add-path/
422+ // trust-grant. A log-only summary is invisible — nobody watches
423+ // ~/.corbits/logs/corbits.log — so these are also queued as transcript rows
424+ // once the shell mounts (see `systemRow` calls after `mountRunnerHost`).
425+ const startupPluginNotices : string [ ] = [ ] ;
426+ const discoveryNotice = formatPluginWarningsSummary ( pluginLoadDiag . warnings ) ;
427+ if ( discoveryNotice !== undefined ) startupPluginNotices . push ( discoveryNotice ) ;
414428 // Mutable list so trusting a project/path plugin can replace a metadata-only stub
415429 // with a fully loaded module without restarting the process.
416430 let livePluginModules = pluginModules ;
@@ -667,6 +681,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
667681 // Tool plugins are wired in only when enabled AND consented.
668682 const toolPluginCandidates = collectToolPlugins ( executablePlugins ( ) ) ;
669683 // Web and tool plugin resolution are independent, so resolve them concurrently.
684+ const toolPluginDiag = createPluginLoadDiagnostics ( ) ;
670685 const [ activeWeb , extraToolPlugins ] = await Promise . all ( [
671686 resolveWebProviderFromPlugins ( {
672687 candidates : webPluginCandidates ,
@@ -676,9 +691,13 @@ export async function runTUI(initialConfig: Config): Promise<number> {
676691 resolveToolPlugins ( {
677692 candidates : toolPluginCandidates ,
678693 pluginConfig : config . settings ?. plugins ?? { } ,
694+ diagnostics : toolPluginDiag ,
679695 } ) ,
680696 ] ) ;
681697 if ( activeWeb !== undefined ) setActiveWebProviderBrand ( webBrand ( activeWeb . name ) ) ;
698+ emitPluginWarningLog ( toolPluginDiag ) ;
699+ const toolPluginNotice = formatPluginWarningsSummary ( toolPluginDiag . warnings ) ;
700+ if ( toolPluginNotice !== undefined ) startupPluginNotices . push ( toolPluginNotice ) ;
682701
683702 // /plugins UI backend: discovered plugin descriptors plus live, persisted
684703 // config (enabled flag, credentials, web override, extra paths) written to the
@@ -745,6 +764,11 @@ export async function runTUI(initialConfig: Config): Promise<number> {
745764 getWebOverride : ( ) => liveWebOverride ,
746765 saveConfig : async ( id , cfg ) => {
747766 livePluginConfig = { ...livePluginConfig , [ id ] : cfg } ;
767+ // Warnings from the trust-grant load below are collected, not logged:
768+ // like `addPath`, the caller has a result channel back to the operator
769+ // (the command surface's `deps.notify`), so fold them into the returned
770+ // message instead of a log line nobody watches.
771+ let trustGrantMessage : string | undefined ;
748772 // Enabling a project/path plugin records trust and full-loads code.
749773 if ( cfg . enabled === true ) {
750774 const stub = livePluginModules . find ( ( m ) => m . manifest ?. id === id ) ;
@@ -766,7 +790,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
766790 origin : stub . origin ,
767791 diagnostics : trustDiag ,
768792 } ) ;
769- emitPluginWarningLog ( trustDiag ) ;
793+ trustGrantMessage = formatPluginWarningsSummary ( trustDiag . warnings ) ;
770794 if ( full !== null ) {
771795 livePluginModules = livePluginModules . map ( ( m ) =>
772796 m . manifest ?. id === id ? full : m ,
@@ -798,6 +822,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
798822 registerCommandPlugin ( mod . commandPlugin ! ) ;
799823 }
800824 await persistPluginSettings ( ) ;
825+ return trustGrantMessage === undefined ? undefined : { message : trustGrantMessage } ;
801826 } ,
802827 setWebOverride : async ( id ) => {
803828 liveWebOverride = id ;
@@ -813,9 +838,13 @@ export async function runTUI(initialConfig: Config): Promise<number> {
813838 { [ id ] : { enabled : true } } ,
814839 { diagnostics : verifyDiag } ,
815840 ) ;
816- emitPluginWarningLog ( verifyDiag ) ;
817841 if ( profiles . length === 0 ) return { ok : false , message : "No valid agent profiles found" } ;
818- return { ok : true , message : `loaded — ${ profiles . length } profile${ profiles . length === 1 ? "" : "s" } ` } ;
842+ // Fold warnings into the message (same pattern as `addPath`) instead of
843+ // logging them: "loaded — N profiles" must not read identically whether
844+ // or not a profile's skill ref actually resolved.
845+ const warnings = formatPluginWarningsSummary ( verifyDiag . warnings ) ;
846+ const base = `loaded — ${ profiles . length } profile${ profiles . length === 1 ? "" : "s" } ` ;
847+ return { ok : true , message : warnings === undefined ? base : `${ base } (${ warnings } )` } ;
819848 }
820849 // Tool plugins verify by loading (the factory must construct without
821850 // error and yield at least one tool).
@@ -867,8 +896,17 @@ export async function runTUI(initialConfig: Config): Promise<number> {
867896 if ( descriptor === undefined ) return { ok : false , message : "Invalid plugin manifest" } ;
868897 // Persist global path trust only once it resolves to a real plugin, so a
869898 // bogus path never leaves a dangling entry. Expand marketplaces so each
870- // member is trusted (exact-path match on reload).
871- const members = await expandPluginPath ( abs ) ;
899+ // member is trusted (exact-path match on reload). `onSkip` collects into
900+ // `addDiag` instead of the default stderr write — same reasoning as
901+ // `loadPluginEntry` above.
902+ const members = await expandPluginPath ( abs , {
903+ onSkip : ( skip : ExpandPluginPathSkip ) => {
904+ const where = skip . resolved !== undefined ? ` → ${ skip . resolved } ` : "" ;
905+ addDiag . warnings . push (
906+ `marketplace source ${ JSON . stringify ( skip . source ) } skipped (${ skip . reason } )${ where } ` ,
907+ ) ;
908+ } ,
909+ } ) ;
872910 pathTrust = await trustPathPlugins ( members . length > 0 ? members : [ abs ] ) ;
873911 // Replace any existing descriptor/candidate with the same id so re-adding
874912 // refreshes rather than duplicates.
@@ -2047,7 +2085,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
20472085 } ,
20482086 setEnabled : async ( id , enabled ) => {
20492087 const existing = pluginsAdmin . getConfig ( ) [ id ] ?? { } ;
2050- await pluginsAdmin . saveConfig ( id , { ...existing , enabled } ) ;
2088+ return ( await pluginsAdmin . saveConfig ( id , { ...existing , enabled } ) ) ?? undefined ;
20512089 } ,
20522090 saveCredentials : async ( id , credentials ) => {
20532091 const existing = pluginsAdmin . getConfig ( ) [ id ] ?? { } ;
@@ -2229,6 +2267,10 @@ export async function runTUI(initialConfig: Config): Promise<number> {
22292267 } ) ;
22302268 } ) ;
22312269
2270+ // Surface fire-and-forget startup plugin diagnostics now that the shell has
2271+ // a transcript to write into (queued above, before `host` existed).
2272+ for ( const notice of startupPluginNotices ) systemRow ( notice ) ;
2273+
22322274 await host . waitUntilExit ( ) ;
22332275 // Quitting mid-stream is an abnormal end for the in-flight cycle: nothing
22342276 // downstream delivers its terminal event once the app is gone.
0 commit comments