yam-flow 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import fsp from 'node:fs/promises';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { buildStrictGateResult, verifyStrictGateResult } from './gate-result.js';
6
7
 
7
8
  export const EXTERNAL_UPDATE_COMPONENTS = ['yam', 'scrapling', 'insane-search'] as const;
8
9
  export type ExternalUpdateComponent = typeof EXTERNAL_UPDATE_COMPONENTS[number];
@@ -83,6 +84,21 @@ export interface ExternalComponentReceipt {
83
84
  persistence?: 'written' | 'failed';
84
85
  receipt_path?: string;
85
86
  receipt_error?: string;
87
+ install_identity?: YamInstallIdentity;
88
+ rollback_identity?: YamInstallIdentity;
89
+ }
90
+
91
+ export interface YamInstallIdentity {
92
+ expected_version: string;
93
+ observed_version: string;
94
+ first_on_path: string;
95
+ canonical_executable: string;
96
+ npm_global_root: string;
97
+ canonical_npm_global_root: string;
98
+ package_root: string;
99
+ package_manifest: string;
100
+ package_entrypoint: string;
101
+ canonical_entrypoint: string;
86
102
  }
87
103
 
88
104
  interface ResolvedDependencies {
@@ -100,6 +116,18 @@ interface ReceiptWriteResult {
100
116
  error: string;
101
117
  }
102
118
 
119
+ interface YamIdentityValidation {
120
+ passed: boolean;
121
+ error: string;
122
+ check: ExternalComponentReceipt['checks'][number];
123
+ identity?: YamInstallIdentity;
124
+ }
125
+
126
+ interface YamRollbackResult {
127
+ ok: boolean;
128
+ identity?: YamInstallIdentity;
129
+ }
130
+
103
131
  const YAM_REGISTRY_URL = 'https://registry.npmjs.org/yam-flow/latest';
104
132
  const SCRAPLING_PYPI_URL = 'https://pypi.org/pypi/scrapling/json';
105
133
  const INSANE_MARKETPLACE = 'gptaku-codex';
@@ -156,6 +184,44 @@ export async function applyExternalUpdates(
156
184
  const receipts: ExternalComponentReceipt[] = [];
157
185
  try {
158
186
  lockHandle = await acquireApplyLock(deps);
187
+ } catch (error) {
188
+ const lockError = boundedNote(errorMessage(error));
189
+ const nextAction = `confirm no updater is running, inspect the existing lock, then remove it manually only if stale: ${deps.paths.lockFile}`;
190
+ const gateResult = buildStrictGateResult({
191
+ gate_id: 'external-update-apply',
192
+ boundary: 'update',
193
+ checks: [{
194
+ id: 'update-lock-acquisition',
195
+ status: 'failed',
196
+ note: lockError,
197
+ required: true
198
+ }],
199
+ blockers: [`update_lock_not_acquired: ${lockError}`],
200
+ evidence: [`requested-components:${components.join(',')}`, `lock-path:${deps.paths.lockFile}`],
201
+ next_action: nextAction
202
+ });
203
+ const gateContract = verifyStrictGateResult(gateResult);
204
+ return {
205
+ schema: 'yam.external-update-apply.v1',
206
+ generated_at: deps.now().toISOString(),
207
+ mutation_authorized: true,
208
+ requested_components: components,
209
+ applied_components: [],
210
+ receipts: [],
211
+ stopped_early: true,
212
+ lock: {
213
+ path: deps.paths.lockFile,
214
+ acquired: false,
215
+ released: false
216
+ },
217
+ gate_result: gateResult,
218
+ gate_contract: gateContract,
219
+ success: false,
220
+ truth_status: 'blocked' as const,
221
+ next_action: nextAction
222
+ };
223
+ }
224
+ try {
159
225
  for (const component of components) {
160
226
  const receipt = component === 'yam'
161
227
  ? await applyYam(currentYamVersion, deps)
@@ -191,6 +257,49 @@ export async function applyExternalUpdates(
191
257
  || receipt.persistence === 'failed'
192
258
  ));
193
259
  const incomplete = receipts.length !== components.length;
260
+ const operationSuccess = !failed && !incomplete && lockReleased;
261
+ const gateBlockers = receipts.flatMap((receipt) => receipt.checks
262
+ .filter((check) => check.status === 'failed')
263
+ .map((check) => `${receipt.component}.${check.id}: ${check.note}`));
264
+ for (const receipt of receipts) {
265
+ if (receipt.persistence === 'failed') gateBlockers.push(`${receipt.component}.receipt_persistence: ${receipt.receipt_error || 'receipt was not persisted'}`);
266
+ if (receipt.outcome === 'manual_plugin_update_required') gateBlockers.push(`${receipt.component}.manual_update_required: ${receipt.error || 'safe in-place plugin update unavailable'}`);
267
+ }
268
+ if (incomplete) gateBlockers.push(`component_inventory_incomplete: applied ${receipts.length} of ${components.length}`);
269
+ if (!lockReleased) gateBlockers.push(`update_lock_not_released: ${deps.paths.lockFile}`);
270
+ const gateResult = buildStrictGateResult({
271
+ gate_id: 'external-update-apply',
272
+ boundary: 'update',
273
+ checks: [
274
+ {
275
+ id: 'component-inventory',
276
+ status: incomplete ? 'failed' : 'passed',
277
+ note: incomplete ? `applied ${receipts.length} of ${components.length} requested components` : 'every requested component produced one outcome receipt',
278
+ required: true
279
+ },
280
+ {
281
+ id: 'component-outcomes',
282
+ status: failed ? 'failed' : 'passed',
283
+ note: failed ? firstFailureNextAction(receipts) : 'all component outcomes passed',
284
+ required: true
285
+ },
286
+ {
287
+ id: 'update-lock-release',
288
+ status: lockReleased ? 'passed' : 'failed',
289
+ note: lockReleased ? 'external update lock was removed' : `inspect the stale lock: ${deps.paths.lockFile}`,
290
+ required: true
291
+ }
292
+ ],
293
+ blockers: gateBlockers,
294
+ evidence: receipts.map((receipt) => `${receipt.component}:${receipt.outcome}:${receipt.persistence || 'not-persisted'}:${receipt.receipt_path || 'no-path'}`),
295
+ next_action: failed
296
+ ? firstFailureNextAction(receipts)
297
+ : !lockReleased
298
+ ? `confirm no updater is running, inspect the stale lock, then remove it manually: ${deps.paths.lockFile}`
299
+ : 'review the component receipts and restart Codex only if yam skills or a Codex plugin changed'
300
+ });
301
+ const gateContract = verifyStrictGateResult(gateResult);
302
+ const success = operationSuccess && gateResult.status === 'passed' && gateContract.valid;
194
303
  return {
195
304
  schema: 'yam.external-update-apply.v1',
196
305
  generated_at: deps.now().toISOString(),
@@ -201,10 +310,13 @@ export async function applyExternalUpdates(
201
310
  stopped_early: incomplete,
202
311
  lock: {
203
312
  path: deps.paths.lockFile,
313
+ acquired: true,
204
314
  released: lockReleased
205
315
  },
206
- success: !failed && !incomplete && lockReleased,
207
- truth_status: failed || incomplete || !lockReleased ? 'blocked' : 'verified',
316
+ gate_result: gateResult,
317
+ gate_contract: gateContract,
318
+ success,
319
+ truth_status: success ? 'verified' : 'blocked',
208
320
  next_action: failed
209
321
  ? firstFailureNextAction(receipts)
210
322
  : !lockReleased
@@ -465,26 +577,49 @@ async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
465
577
  }
466
578
  sideEffects.push(`globally installed ${exactPackage}`);
467
579
 
580
+ let installIdentity: YamInstallIdentity | undefined;
581
+ let rollbackIdentity: YamInstallIdentity | undefined;
582
+ const identityValidation = await validateEffectiveYamIdentity(
583
+ check.latest_version,
584
+ deps,
585
+ 'yam_effective_identity'
586
+ );
587
+ checks.push(identityValidation.check);
588
+ if (identityValidation.passed) installIdentity = identityValidation.identity;
589
+
468
590
  const postCommands: Array<[string, string[]]> = [
469
591
  ['yam_install_skills', ['install']],
470
592
  ['yam_version', ['version']],
471
593
  ['yam_status', ['status']]
472
594
  ];
473
- let postFailure = '';
474
- for (const [id, args] of postCommands) {
475
- const result = await deps.run('yam', args, { env: deps.env, timeout: 180000 });
476
- checks.push(commandCheck(id, result));
477
- if (!result.ok || (id === 'yam_version' && extractVersion(result.stdout) !== check.latest_version)) {
478
- postFailure = `${id} failed: ${commandNote(result)}`;
479
- break;
595
+ let postFailure = identityValidation.passed
596
+ ? ''
597
+ : `yam_effective_identity failed: ${identityValidation.error}`;
598
+ if (!postFailure && installIdentity) {
599
+ for (const [id, args] of postCommands) {
600
+ const result = await deps.run(installIdentity.first_on_path, args, { env: deps.env, timeout: 180000 });
601
+ checks.push(commandCheck(id, result));
602
+ if (!result.ok || (id === 'yam_version' && extractVersion(result.stdout) !== check.latest_version)) {
603
+ postFailure = `${id} failed: ${commandNote(result)}`;
604
+ break;
605
+ }
480
606
  }
481
607
  }
608
+ if (!postFailure && installIdentity) {
609
+ const doctor = await deps.run(installIdentity.first_on_path, ['doctor', '--json'], { env: deps.env, timeout: 180000 });
610
+ const validation = doctorValidation('yam_finalize_doctor', doctor);
611
+ checks.push(validation.check);
612
+ if (!validation.passed) postFailure = `yam_finalize_doctor failed: ${validation.error}`;
613
+ }
482
614
  if (postFailure) {
483
615
  const rollback = await rollbackYam(check.installed_version, deps, checks);
616
+ rollbackIdentity = rollback.identity;
484
617
  return persistReceipt(baseReceipt(check, 'failed', checks, {
485
618
  ...yamRollback(check.installed_version),
486
- automatic: rollback
619
+ automatic: rollback.ok
487
620
  }, {
621
+ ...(installIdentity ? { install_identity: installIdentity } : {}),
622
+ ...(rollbackIdentity ? { rollback_identity: rollbackIdentity } : {}),
488
623
  side_effects: sideEffects,
489
624
  error: postFailure
490
625
  }), deps);
@@ -492,16 +627,18 @@ async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
492
627
 
493
628
  const receipt = baseReceipt(check, 'updated', checks, yamRollback(check.installed_version), {
494
629
  installed_version: check.latest_version,
630
+ ...(installIdentity ? { install_identity: installIdentity } : {}),
495
631
  side_effects: sideEffects,
496
632
  truth_status: 'verified'
497
633
  });
498
634
  const persisted = await persistReceipt(receipt, deps);
499
635
  if (persisted.persistence === 'failed') {
500
636
  const rollback = await rollbackYam(check.installed_version, deps, persisted.checks);
501
- persisted.rollback_hint.automatic = rollback;
637
+ persisted.rollback_hint.automatic = rollback.ok;
638
+ if (rollback.identity) persisted.rollback_identity = rollback.identity;
502
639
  persisted.outcome = 'failed';
503
640
  persisted.truth_status = 'blocked';
504
- persisted.error = `update completed but receipt persistence failed; automatic rollback ${rollback ? 'passed' : 'failed'}`;
641
+ persisted.error = `update completed but receipt persistence failed; automatic rollback ${rollback.ok ? 'passed' : 'failed'}`;
505
642
  }
506
643
  return persisted;
507
644
  }
@@ -510,21 +647,155 @@ async function rollbackYam(
510
647
  previousVersion: string,
511
648
  deps: ResolvedDependencies,
512
649
  checks: ExternalComponentReceipt['checks']
513
- ) {
514
- if (!previousVersion) return false;
650
+ ): Promise<YamRollbackResult> {
651
+ if (!previousVersion) return { ok: false };
515
652
  const result = await deps.run('npm', ['install', '-g', `yam-flow@${previousVersion}`], {
516
653
  env: deps.env,
517
654
  timeout: 600000
518
655
  });
519
656
  checks.push(commandCheck('automatic_yam_rollback', result));
520
- if (!result.ok) return false;
521
- const install = await deps.run('yam', ['install'], { env: deps.env, timeout: 180000 });
522
- const version = await deps.run('yam', ['version'], { env: deps.env, timeout: 30000 });
523
- const status = await deps.run('yam', ['status'], { env: deps.env, timeout: 120000 });
657
+ if (!result.ok) return { ok: false };
658
+
659
+ const identityValidation = await validateEffectiveYamIdentity(
660
+ previousVersion,
661
+ deps,
662
+ 'automatic_yam_rollback_identity'
663
+ );
664
+ checks.push(identityValidation.check);
665
+ if (!identityValidation.passed || !identityValidation.identity) return { ok: false };
666
+
667
+ const executable = identityValidation.identity.first_on_path;
668
+ const install = await deps.run(executable, ['install'], { env: deps.env, timeout: 180000 });
669
+ const version = await deps.run(executable, ['version'], { env: deps.env, timeout: 30000 });
670
+ const status = await deps.run(executable, ['status'], { env: deps.env, timeout: 120000 });
671
+ const doctor = await deps.run(executable, ['doctor', '--json'], { env: deps.env, timeout: 180000 });
672
+ const doctorResult = doctorValidation('automatic_yam_rollback_doctor', doctor);
524
673
  checks.push(commandCheck('automatic_yam_rollback_skills', install));
525
674
  checks.push(commandCheck('automatic_yam_rollback_version', version));
526
675
  checks.push(commandCheck('automatic_yam_rollback_status', status));
527
- return install.ok && version.ok && extractVersion(version.stdout) === previousVersion && status.ok;
676
+ checks.push(doctorResult.check);
677
+ return {
678
+ ok: (
679
+ install.ok
680
+ && version.ok
681
+ && extractVersion(version.stdout) === previousVersion
682
+ && status.ok
683
+ && doctorResult.passed
684
+ ),
685
+ identity: identityValidation.identity
686
+ };
687
+ }
688
+
689
+ async function validateEffectiveYamIdentity(
690
+ expectedVersion: string,
691
+ deps: ResolvedDependencies,
692
+ id: string
693
+ ): Promise<YamIdentityValidation> {
694
+ try {
695
+ const npmRootResult = await deps.run('npm', ['root', '-g'], { env: deps.env, timeout: 30000 });
696
+ if (!npmRootResult.ok) {
697
+ throw new Error(`npm global root lookup failed: ${commandNote(npmRootResult)}`);
698
+ }
699
+ const npmGlobalRoot = firstOutputLine(npmRootResult.stdout);
700
+ if (!npmGlobalRoot || !path.isAbsolute(npmGlobalRoot)) {
701
+ throw new Error('npm global root lookup did not return one absolute path');
702
+ }
703
+
704
+ const packageRoot = path.join(path.resolve(npmGlobalRoot), 'yam-flow');
705
+ const packageManifest = path.join(packageRoot, 'package.json');
706
+ let manifest: JsonRecord;
707
+ try {
708
+ const parsed = JSON.parse(await fsp.readFile(packageManifest, 'utf8'));
709
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
710
+ manifest = parsed;
711
+ } catch {
712
+ throw new Error(`yam-flow package manifest is missing or malformed: ${packageManifest}`);
713
+ }
714
+ if (manifest.name !== 'yam-flow') {
715
+ throw new Error(`npm global package manifest has unexpected name at ${packageManifest}`);
716
+ }
717
+ const manifestVersion = requireStableVersion(manifest.version, 'yam-flow package manifest version');
718
+ if (manifestVersion !== expectedVersion) {
719
+ throw new Error(`yam-flow package manifest version ${manifestVersion} does not match expected ${expectedVersion}`);
720
+ }
721
+
722
+ const binValue = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.yam;
723
+ if (typeof binValue !== 'string' || !binValue.trim()) {
724
+ throw new Error(`yam-flow package manifest has no usable yam bin entry: ${packageManifest}`);
725
+ }
726
+ const packageEntrypoint = path.resolve(packageRoot, binValue);
727
+ assertChildPath(packageRoot, packageEntrypoint, 'yam-flow package entrypoint');
728
+
729
+ const whichResult = await deps.run('which', ['yam'], { env: deps.env, timeout: 10000 });
730
+ if (!whichResult.ok) {
731
+ throw new Error(`first yam on PATH could not be resolved: ${commandNote(whichResult)}`);
732
+ }
733
+ const firstOnPath = firstOutputLine(whichResult.stdout);
734
+ if (!firstOnPath || !path.isAbsolute(firstOnPath)) {
735
+ throw new Error('first yam on PATH was not reported as one absolute path');
736
+ }
737
+
738
+ let canonicalNpmGlobalRoot: string;
739
+ let canonicalPackageRoot: string;
740
+ let canonicalEntrypoint: string;
741
+ let canonicalExecutable: string;
742
+ try {
743
+ [canonicalNpmGlobalRoot, canonicalPackageRoot, canonicalEntrypoint, canonicalExecutable] = await Promise.all([
744
+ fsp.realpath(npmGlobalRoot),
745
+ fsp.realpath(packageRoot),
746
+ fsp.realpath(packageEntrypoint),
747
+ fsp.realpath(firstOnPath)
748
+ ]);
749
+ } catch {
750
+ throw new Error('yam install identity contains an unreadable package path, entrypoint, or PATH executable');
751
+ }
752
+ assertChildPath(canonicalNpmGlobalRoot, canonicalPackageRoot, 'canonical yam-flow package root');
753
+ assertChildPath(canonicalPackageRoot, canonicalEntrypoint, 'canonical yam-flow package entrypoint');
754
+ if (canonicalExecutable !== canonicalEntrypoint) {
755
+ throw new Error(
756
+ `first yam on PATH is shadowed: ${firstOnPath} resolves to ${canonicalExecutable}, expected ${canonicalEntrypoint}; reorder PATH or remove the stale shim before retrying`
757
+ );
758
+ }
759
+
760
+ const versionResult = await deps.run(firstOnPath, ['version'], { env: deps.env, timeout: 30000 });
761
+ if (!versionResult.ok) {
762
+ throw new Error(`effective yam version check failed: ${commandNote(versionResult)}`);
763
+ }
764
+ const observedVersion = requireStableVersion(extractVersion(versionResult.stdout), 'effective yam version');
765
+ if (observedVersion !== expectedVersion) {
766
+ throw new Error(`effective yam version ${observedVersion} does not match expected ${expectedVersion}`);
767
+ }
768
+
769
+ const identity: YamInstallIdentity = {
770
+ expected_version: expectedVersion,
771
+ observed_version: observedVersion,
772
+ first_on_path: firstOnPath,
773
+ canonical_executable: canonicalExecutable,
774
+ npm_global_root: path.resolve(npmGlobalRoot),
775
+ canonical_npm_global_root: canonicalNpmGlobalRoot,
776
+ package_root: packageRoot,
777
+ package_manifest: packageManifest,
778
+ package_entrypoint: packageEntrypoint,
779
+ canonical_entrypoint: canonicalEntrypoint
780
+ };
781
+ return {
782
+ passed: true,
783
+ error: '',
784
+ check: {
785
+ id,
786
+ status: 'passed',
787
+ note: boundedNote(`verified yam install identity: ${JSON.stringify(identity)}`)
788
+ },
789
+ identity
790
+ };
791
+ } catch (error) {
792
+ const note = boundedNote(errorMessage(error));
793
+ return {
794
+ passed: false,
795
+ error: note,
796
+ check: { id, status: 'failed', note }
797
+ };
798
+ }
528
799
  }
529
800
 
530
801
  async function applyScrapling(deps: ResolvedDependencies) {
@@ -967,7 +1238,7 @@ function yamRollback(previousVersion: string) {
967
1238
  automatic: false,
968
1239
  previous_target: previousVersion,
969
1240
  guidance: previousVersion
970
- ? `Reinstall the exact prior package with \`npm install -g yam-flow@${previousVersion}\`, then run \`yam install\`, \`yam version\`, and \`yam status\`.`
1241
+ ? `Reinstall the exact prior package with \`npm install -g yam-flow@${previousVersion}\`, then run \`yam install\`, \`yam version\`, \`yam status\`, and \`yam doctor --json\`.`
971
1242
  : 'Inspect the global npm installation before attempting a manual rollback.'
972
1243
  };
973
1244
  }
@@ -1008,6 +1279,92 @@ function commandCheck(id: string, result: ExternalCommandResult): ExternalCompon
1008
1279
  };
1009
1280
  }
1010
1281
 
1282
+ function doctorValidation(id: string, result: ExternalCommandResult) {
1283
+ if (!result.ok) {
1284
+ const note = boundedNote(`command failed: ${commandNote(result)}`);
1285
+ return {
1286
+ passed: false,
1287
+ error: note,
1288
+ check: { id, status: 'failed' as const, note }
1289
+ };
1290
+ }
1291
+
1292
+ let report: JsonRecord;
1293
+ try {
1294
+ const parsed = JSON.parse(String(result.stdout || ''));
1295
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1296
+ throw new Error('not an object');
1297
+ }
1298
+ report = parsed;
1299
+ } catch {
1300
+ const note = 'yam doctor returned invalid JSON';
1301
+ return {
1302
+ passed: false,
1303
+ error: note,
1304
+ check: { id, status: 'failed' as const, note }
1305
+ };
1306
+ }
1307
+
1308
+ if (report.schema !== 'yam.doctor.v1') {
1309
+ const note = 'yam doctor returned an unexpected schema';
1310
+ return {
1311
+ passed: false,
1312
+ error: note,
1313
+ check: { id, status: 'failed' as const, note }
1314
+ };
1315
+ }
1316
+ if (report.ok !== true) {
1317
+ const note = doctorFailureDiagnostic(report);
1318
+ return {
1319
+ passed: false,
1320
+ error: note,
1321
+ check: { id, status: 'failed' as const, note }
1322
+ };
1323
+ }
1324
+
1325
+ const note = 'yam.doctor.v1 reported ok=true';
1326
+ return {
1327
+ passed: true,
1328
+ error: '',
1329
+ check: { id, status: 'passed' as const, note }
1330
+ };
1331
+ }
1332
+
1333
+ function doctorFailureDiagnostic(report: JsonRecord) {
1334
+ const detail = arrayValue(report.nextActionDetails).find((item) => (
1335
+ item
1336
+ && typeof item === 'object'
1337
+ && !Array.isArray(item)
1338
+ && [item.reason, item.next_action, item.nextAction, item.command].some((value) => cleanDiagnosticValue(value))
1339
+ ));
1340
+ const issue = cleanDiagnosticValue(detail?.reason) || firstDiagnosticValue(report.issues);
1341
+ const nextAction = cleanDiagnosticValue(detail?.next_action || detail?.nextAction)
1342
+ || firstDiagnosticValue(report.nextActions);
1343
+ const command = cleanDiagnosticValue(detail?.command);
1344
+ const fragments = [
1345
+ issue ? `issue: ${issue}` : '',
1346
+ nextAction ? `next: ${nextAction}` : '',
1347
+ command ? `command: ${command}` : ''
1348
+ ].filter(Boolean);
1349
+ return boundedNote(
1350
+ fragments.length
1351
+ ? `yam.doctor.v1 reported ok=false; ${fragments.join('; ')}`
1352
+ : 'yam.doctor.v1 reported ok=false'
1353
+ );
1354
+ }
1355
+
1356
+ function firstDiagnosticValue(value: unknown) {
1357
+ return arrayValue(value).map(cleanDiagnosticValue).find(Boolean) || '';
1358
+ }
1359
+
1360
+ function cleanDiagnosticValue(value: unknown) {
1361
+ return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
1362
+ }
1363
+
1364
+ function boundedNote(text: string) {
1365
+ return redactSensitiveText(String(text || '')).slice(0, 800);
1366
+ }
1367
+
1011
1368
  function commandNote(result: ExternalCommandResult) {
1012
1369
  const text = redactSensitiveText([result.stdout, result.stderr].filter(Boolean).join('\n'));
1013
1370
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
@@ -1055,6 +1412,10 @@ function extractVersion(text: string) {
1055
1412
  return String(text || '').match(/\b\d+(?:\.\d+){1,3}\b/)?.[0] || '';
1056
1413
  }
1057
1414
 
1415
+ function firstOutputLine(text: string) {
1416
+ return String(text || '').split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
1417
+ }
1418
+
1058
1419
  function compareVersions(left: string, right: string) {
1059
1420
  const a = left.split('.').map(Number);
1060
1421
  const b = right.split('.').map(Number);
@@ -1131,6 +1492,7 @@ async function unlinkIfExists(file: string) {
1131
1492
  function redactSensitiveText(text: string) {
1132
1493
  return String(text || '')
1133
1494
  .replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
1495
+ .replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY))\s*[:=]\s*\S+/gi, '$1=[redacted]')
1134
1496
  .replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
1135
1497
  .replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
1136
1498
  .replace(/\b(npm_[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[redacted-token]');
@@ -1143,8 +1505,19 @@ function firstFailureNextAction(receipts: ExternalComponentReceipt[]) {
1143
1505
  || item.persistence === 'failed'
1144
1506
  ));
1145
1507
  if (!receipt) return 'inspect the incomplete component inventory before retrying';
1146
- if (receipt.outcome === 'manual_plugin_update_required') {
1147
- return 'update Insane Search through a reviewed official Codex plugin workflow; do not remove-first or edit the plugin cache directly';
1148
- }
1149
- return receipt.rollback_hint.guidance || `resolve the ${receipt.component} failure before retrying`;
1508
+ const deepestFailedCheck = [...receipt.checks].reverse().find((check) => check.status === 'failed');
1509
+ const deepestCause = deepestFailedCheck
1510
+ ? `${deepestFailedCheck.id} failed: ${deepestFailedCheck.note}`
1511
+ : receipt.persistence === 'failed' && receipt.receipt_error
1512
+ ? `receipt persistence failed: ${receipt.receipt_error}`
1513
+ : receipt.error || `${receipt.component} update failed`;
1514
+ const persistenceContext = deepestFailedCheck && receipt.persistence === 'failed' && receipt.receipt_error
1515
+ ? `; secondary receipt persistence failure: ${receipt.receipt_error}`
1516
+ : '';
1517
+ const cause = boundedNote(`${deepestCause}${persistenceContext}`).slice(0, 420);
1518
+ const recoveryGuidance = receipt.outcome === 'manual_plugin_update_required'
1519
+ ? 'Update Insane Search through a reviewed official Codex plugin workflow; do not remove-first or edit the plugin cache directly.'
1520
+ : receipt.rollback_hint.guidance || `resolve the ${receipt.component} failure before retrying`;
1521
+ const recovery = boundedNote(recoveryGuidance).slice(0, 340);
1522
+ return boundedNote(`resolve ${receipt.component} failure: ${cause}. ${recovery}`);
1150
1523
  }