yam-flow 2.7.0 → 2.8.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.
Files changed (40) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +29 -0
  3. package/COMMANDS.md +42 -5
  4. package/README.md +40 -8
  5. package/ROADMAP.md +17 -6
  6. package/dist/bin/yam.js +441 -70
  7. package/dist/lib/external-updates.d.ts +9 -0
  8. package/dist/lib/external-updates.js +232 -4
  9. package/dist/lib/next-step.d.ts +61 -0
  10. package/dist/lib/next-step.js +340 -0
  11. package/dist/lib/release-auth-readiness.d.ts +81 -0
  12. package/dist/lib/release-auth-readiness.js +106 -0
  13. package/dist/lib/release-registry-status.d.ts +18 -0
  14. package/dist/lib/release-registry-status.js +37 -0
  15. package/dist/lib/scout-receipt.d.ts +89 -0
  16. package/dist/lib/scout-receipt.js +542 -0
  17. package/dist/lib/skill-installation.d.ts +30 -0
  18. package/dist/lib/skill-installation.js +470 -82
  19. package/dist/lib/trust-kernel.d.ts +2 -0
  20. package/dist/lib/trust-kernel.js +21 -2
  21. package/package.json +22 -3
  22. package/references/final-report.md +15 -1
  23. package/references/next-step.md +67 -0
  24. package/references/scout-wide-scan.md +16 -0
  25. package/references/study-note.md +4 -2
  26. package/references/tool-trust-layer.md +2 -0
  27. package/skills/deep/SKILL.md +2 -0
  28. package/skills/mission/SKILL.md +2 -0
  29. package/skills/quick/SKILL.md +1 -0
  30. package/skills/scout/SKILL.md +19 -9
  31. package/skills/ueye/SKILL.md +1 -0
  32. package/src/bin/yam.ts +445 -68
  33. package/src/lib/external-updates.ts +262 -4
  34. package/src/lib/next-step.ts +397 -0
  35. package/src/lib/release-auth-readiness.ts +148 -0
  36. package/src/lib/release-registry-status.ts +55 -0
  37. package/src/lib/scout-receipt.ts +593 -0
  38. package/src/lib/skill-installation.ts +542 -87
  39. package/src/lib/trust-kernel.ts +25 -2
  40. package/yam.manifest.json +4 -3
@@ -54,6 +54,14 @@ export interface ExternalComponentCheck {
54
54
  drift: boolean;
55
55
  note: string;
56
56
  };
57
+ source_receipt?: {
58
+ canonical_url: string;
59
+ retrieved_at: string;
60
+ access_path: string;
61
+ version: string;
62
+ revision: string;
63
+ integrity: string[];
64
+ };
57
65
  executable?: string;
58
66
  error?: string;
59
67
  }
@@ -68,6 +76,7 @@ export interface ExternalComponentReceipt {
68
76
  installed_version: string;
69
77
  source: string;
70
78
  source_revision: JsonRecord | null;
79
+ source_receipt: JsonRecord | null;
71
80
  checks: Array<{
72
81
  id: string;
73
82
  status: 'passed' | 'failed' | 'skipped';
@@ -129,7 +138,9 @@ interface YamRollbackResult {
129
138
  }
130
139
 
131
140
  const YAM_REGISTRY_URL = 'https://registry.npmjs.org/yam-flow/latest';
141
+ const NPM_REGISTRY = 'https://registry.npmjs.org/';
132
142
  const SCRAPLING_PYPI_URL = 'https://pypi.org/pypi/scrapling/json';
143
+ const SCRAPLING_PYPI_INDEX = 'https://pypi.org/simple';
133
144
  const INSANE_MARKETPLACE = 'gptaku-codex';
134
145
  const INSANE_PLUGIN_ID = 'insane-search-codex@gptaku-codex';
135
146
  const INSANE_MARKETPLACE_SOURCE = 'https://github.com/fivetaku/gptaku-plugins-codex.git';
@@ -402,7 +413,19 @@ async function checkYam(currentYamVersion: string, deps: ResolvedDependencies):
402
413
  const data = await deps.fetchJson(YAM_REGISTRY_URL);
403
414
  const latest = requireStableVersion(data?.version, 'yam registry version');
404
415
  const installed = requireStableVersion(currentYamVersion, 'installed yam version');
405
- return componentCheck('yam', installed, latest, 'npm:yam-flow');
416
+ const integrity = requireIntegrityValue(data?.dist?.integrity, 'yam registry integrity');
417
+ const revision = requireGitRevision(data?.gitHead, 'yam registry gitHead');
418
+ return {
419
+ ...componentCheck('yam', installed, latest, 'npm:yam-flow'),
420
+ source_receipt: {
421
+ canonical_url: YAM_REGISTRY_URL,
422
+ retrieved_at: deps.now().toISOString(),
423
+ access_path: 'npm registry latest endpoint',
424
+ version: latest,
425
+ revision,
426
+ integrity: [integrity]
427
+ }
428
+ };
406
429
  } catch (error) {
407
430
  return failedCheck('yam', currentYamVersion, 'npm:yam-flow', error);
408
431
  }
@@ -412,6 +435,7 @@ async function checkScrapling(deps: ResolvedDependencies): Promise<ExternalCompo
412
435
  try {
413
436
  const data = await deps.fetchJson(SCRAPLING_PYPI_URL);
414
437
  const latest = requireStableVersion(data?.info?.version, 'Scrapling PyPI version');
438
+ const releaseIntegrity = pypiReleaseIntegrity(data?.urls, latest);
415
439
  const executable = deps.paths.scraplingBin;
416
440
  const versionResult = await deps.run(executable, ['--version'], { env: deps.env, timeout: 30000 });
417
441
  if (!versionResult.ok) {
@@ -422,12 +446,28 @@ async function checkScrapling(deps: ResolvedDependencies): Promise<ExternalCompo
422
446
  update_available: true,
423
447
  status: 'not_installed',
424
448
  source: 'pypi:scrapling',
449
+ source_receipt: {
450
+ canonical_url: SCRAPLING_PYPI_URL,
451
+ retrieved_at: deps.now().toISOString(),
452
+ access_path: 'PyPI project JSON endpoint',
453
+ version: latest,
454
+ revision: '',
455
+ integrity: releaseIntegrity
456
+ },
425
457
  executable
426
458
  };
427
459
  }
428
460
  const installed = requireStableVersion(extractVersion(versionResult.stdout), 'installed Scrapling version');
429
461
  return {
430
462
  ...componentCheck('scrapling', installed, latest, 'pypi:scrapling'),
463
+ source_receipt: {
464
+ canonical_url: SCRAPLING_PYPI_URL,
465
+ retrieved_at: deps.now().toISOString(),
466
+ access_path: 'PyPI project JSON endpoint',
467
+ version: latest,
468
+ revision: '',
469
+ integrity: releaseIntegrity
470
+ },
431
471
  executable
432
472
  };
433
473
  } catch (error) {
@@ -510,6 +550,14 @@ async function checkInsaneSearch(deps: ResolvedDependencies): Promise<ExternalCo
510
550
  note: installed && compareVersions(installed, latest) === 0
511
551
  ? 'marketplace revision drift alone is not treated as an Insane Search update'
512
552
  : 'plugin update availability is based on the official plugin manifest version'
553
+ },
554
+ source_receipt: {
555
+ canonical_url: manifestUrl,
556
+ retrieved_at: deps.now().toISOString(),
557
+ access_path: 'Git-pinned raw marketplace manifest',
558
+ version: latest,
559
+ revision: remoteRevision,
560
+ integrity: [`git_commit:${remoteRevision.toLowerCase()}`]
513
561
  }
514
562
  };
515
563
  } catch (error) {
@@ -565,7 +613,13 @@ async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
565
613
  const checks: ExternalComponentReceipt['checks'] = [];
566
614
  const sideEffects: string[] = [];
567
615
  const exactPackage = `yam-flow@${check.latest_version}`;
568
- const install = await deps.run('npm', ['install', '-g', exactPackage], {
616
+ const install = await deps.run('npm', [
617
+ 'install',
618
+ '-g',
619
+ exactPackage,
620
+ '--registry',
621
+ NPM_REGISTRY
622
+ ], {
569
623
  env: deps.env,
570
624
  timeout: 600000
571
625
  });
@@ -649,7 +703,13 @@ async function rollbackYam(
649
703
  checks: ExternalComponentReceipt['checks']
650
704
  ): Promise<YamRollbackResult> {
651
705
  if (!previousVersion) return { ok: false };
652
- const result = await deps.run('npm', ['install', '-g', `yam-flow@${previousVersion}`], {
706
+ const result = await deps.run('npm', [
707
+ 'install',
708
+ '-g',
709
+ `yam-flow@${previousVersion}`,
710
+ '--registry',
711
+ NPM_REGISTRY
712
+ ], {
653
713
  env: deps.env,
654
714
  timeout: 600000
655
715
  });
@@ -831,7 +891,15 @@ async function applyScrapling(deps: ResolvedDependencies) {
831
891
  const candidatePython = path.join(candidate, 'bin', 'python');
832
892
  const candidateScrapling = path.join(candidate, 'bin', 'scrapling');
833
893
  const packageSpec = `scrapling[fetchers]==${check.latest_version}`;
834
- const install = await deps.run(candidatePython, ['-m', 'pip', 'install', packageSpec], {
894
+ const install = await deps.run(candidatePython, [
895
+ '-m',
896
+ 'pip',
897
+ 'install',
898
+ '--isolated',
899
+ '--index-url',
900
+ SCRAPLING_PYPI_INDEX,
901
+ packageSpec
902
+ ], {
835
903
  env: deps.env,
836
904
  timeout: 600000
837
905
  });
@@ -970,6 +1038,16 @@ async function applyInsaneSearch(deps: ResolvedDependencies) {
970
1038
  }), deps);
971
1039
  }
972
1040
 
1041
+ const pinnedSource = await verifyUpgradedInsaneSource(check, deps, 'pre_add_source_revision');
1042
+ checks.push(pinnedSource.check);
1043
+ if (!pinnedSource.ok) {
1044
+ return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
1045
+ installed_version: check.installed_version,
1046
+ side_effects: ['The official Codex marketplace snapshot was upgraded; plugin add was blocked because the post-upgrade source did not match the checked revision.'],
1047
+ error: pinnedSource.error
1048
+ }), deps);
1049
+ }
1050
+
973
1051
  let observation = await installedInsaneVersion(deps, checks, 'plugin_list_after_marketplace_upgrade');
974
1052
  if (!observation.ok) {
975
1053
  return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
@@ -1011,6 +1089,21 @@ async function applyInsaneSearch(deps: ResolvedDependencies) {
1011
1089
  }), deps);
1012
1090
  }
1013
1091
 
1092
+ const finalPinnedSource = await verifyUpgradedInsaneSource(check, deps, 'final_source_revision');
1093
+ checks.push(finalPinnedSource.check);
1094
+ if (!finalPinnedSource.ok) {
1095
+ return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
1096
+ installed_version: installedAfter,
1097
+ side_effects: [
1098
+ `upgraded official marketplace ${INSANE_MARKETPLACE}`,
1099
+ installedAfter === check.latest_version
1100
+ ? `the plugin reached ${installedAfter}, but final source provenance changed and completion was blocked`
1101
+ : 'plugin state did not reach the checked version'
1102
+ ],
1103
+ error: finalPinnedSource.error
1104
+ }), deps);
1105
+ }
1106
+
1014
1107
  return persistReceipt(baseReceipt(check, 'updated', checks, insaneRollback(check.installed_version), {
1015
1108
  installed_version: installedAfter,
1016
1109
  side_effects: [
@@ -1021,6 +1114,131 @@ async function applyInsaneSearch(deps: ResolvedDependencies) {
1021
1114
  }), deps);
1022
1115
  }
1023
1116
 
1117
+ async function verifyUpgradedInsaneSource(
1118
+ check: ExternalComponentCheck,
1119
+ deps: ResolvedDependencies,
1120
+ id: string
1121
+ ) {
1122
+ try {
1123
+ const expectedRevision = requireGitRevision(
1124
+ check.source_receipt?.revision,
1125
+ 'checked Insane Search source revision'
1126
+ );
1127
+ const marketplaceList = await deps.run('codex', ['plugin', 'marketplace', 'list', '--json'], {
1128
+ env: deps.env,
1129
+ timeout: 30000
1130
+ });
1131
+ if (!marketplaceList.ok) {
1132
+ throw new Error(`post-upgrade marketplace state could not be observed: ${commandNote(marketplaceList)}`);
1133
+ }
1134
+ const parsedMarketplaces = parseJsonOutput(marketplaceList.stdout, 'post-upgrade codex plugin marketplace list');
1135
+ const marketplaceEntry = arrayValue(parsedMarketplaces?.marketplaces).find((item) => item?.name === INSANE_MARKETPLACE);
1136
+ if (!marketplaceEntry) throw new Error(`post-upgrade marketplace is missing: ${INSANE_MARKETPLACE}`);
1137
+ const configuredSource = String(marketplaceEntry?.marketplaceSource?.source || '');
1138
+ if (
1139
+ marketplaceEntry?.marketplaceSource?.sourceType !== 'git'
1140
+ || configuredSource !== INSANE_MARKETPLACE_SOURCE
1141
+ ) {
1142
+ throw new Error(`refusing unexpected post-upgrade ${INSANE_MARKETPLACE} marketplace source`);
1143
+ }
1144
+ const marketplaceRoot = String(marketplaceEntry.root || '');
1145
+ if (!marketplaceRoot || !isSafeMarketplaceRoot(marketplaceRoot, deps.homeDir)) {
1146
+ throw new Error(`refusing unsafe post-upgrade ${INSANE_MARKETPLACE} marketplace root`);
1147
+ }
1148
+ const localRevisionResult = await deps.run('git', ['-C', marketplaceRoot, 'rev-parse', 'HEAD'], {
1149
+ env: deps.env,
1150
+ timeout: 30000
1151
+ });
1152
+ if (!localRevisionResult.ok) {
1153
+ throw new Error(`post-upgrade marketplace revision could not be read: ${commandNote(localRevisionResult)}`);
1154
+ }
1155
+ const actualRevision = requireGitRevision(
1156
+ localRevisionResult.stdout.trim(),
1157
+ 'post-upgrade Insane Search marketplace revision'
1158
+ );
1159
+ const statusResult = await deps.run('git', [
1160
+ '-C',
1161
+ marketplaceRoot,
1162
+ 'status',
1163
+ '--porcelain=v1',
1164
+ '--untracked-files=all',
1165
+ '--ignored=matching'
1166
+ ], { env: deps.env, timeout: 30000 });
1167
+ if (!statusResult.ok) {
1168
+ throw new Error(`post-upgrade marketplace cleanliness could not be observed: ${commandNote(statusResult)}`);
1169
+ }
1170
+ if (statusResult.stdout.trim()) {
1171
+ throw new Error('post-upgrade Insane Search marketplace working tree is dirty; refusing to bind plugin install to a clean commit receipt');
1172
+ }
1173
+ const trackedManifest = await deps.run('git', [
1174
+ '-C',
1175
+ marketplaceRoot,
1176
+ 'ls-files',
1177
+ '--error-unmatch',
1178
+ '--',
1179
+ INSANE_MANIFEST_PATH
1180
+ ], { env: deps.env, timeout: 30000 });
1181
+ if (!trackedManifest.ok) {
1182
+ throw new Error(`local Insane Search manifest is not tracked at the checked commit: ${commandNote(trackedManifest)}`);
1183
+ }
1184
+ const localManifestPath = path.join(marketplaceRoot, INSANE_MANIFEST_PATH);
1185
+ assertChildPath(marketplaceRoot, localManifestPath, 'local Insane Search manifest');
1186
+ const localManifestStat = await fsp.lstat(localManifestPath);
1187
+ if (!localManifestStat.isFile() || localManifestStat.isSymbolicLink() || localManifestStat.size > 1024 * 1024) {
1188
+ throw new Error('local Insane Search manifest must be a regular tracked file no larger than 1 MiB');
1189
+ }
1190
+ const localManifestBytes = await fsp.readFile(localManifestPath, 'utf8');
1191
+ const committedManifest = await deps.run('git', [
1192
+ '-C',
1193
+ marketplaceRoot,
1194
+ 'show',
1195
+ `${actualRevision}:${INSANE_MANIFEST_PATH}`
1196
+ ], { env: deps.env, timeout: 30000 });
1197
+ if (!committedManifest.ok) {
1198
+ throw new Error(`checked Insane Search manifest bytes could not be read from Git: ${commandNote(committedManifest)}`);
1199
+ }
1200
+ if (localManifestBytes !== committedManifest.stdout) {
1201
+ throw new Error('local Insane Search manifest bytes do not match the checked Git commit');
1202
+ }
1203
+ const localManifest = parseJsonOutput(localManifestBytes, 'local Insane Search manifest');
1204
+ const localVersion = requireStableVersion(localManifest?.version, 'local Insane Search manifest version');
1205
+ const manifestUrl = `${INSANE_MANIFEST_ROOT}/${actualRevision}/${INSANE_MANIFEST_PATH}`;
1206
+ const manifest = await deps.fetchJson(manifestUrl);
1207
+ const actualVersion = requireStableVersion(manifest?.version, 'post-upgrade Insane Search manifest version');
1208
+ if (actualRevision !== expectedRevision) {
1209
+ throw new Error(
1210
+ `Insane Search source changed between check and upgrade: checked ${expectedRevision}, observed ${actualRevision}; rerun the read-only check before retrying`
1211
+ );
1212
+ }
1213
+ if (actualVersion !== check.latest_version) {
1214
+ throw new Error(
1215
+ `post-upgrade Insane Search manifest version ${actualVersion} does not match checked ${check.latest_version}`
1216
+ );
1217
+ }
1218
+ if (localVersion !== actualVersion) {
1219
+ throw new Error(
1220
+ `local Insane Search manifest version ${localVersion} does not match pinned source version ${actualVersion}`
1221
+ );
1222
+ }
1223
+ return {
1224
+ ok: true,
1225
+ error: '',
1226
+ check: {
1227
+ id,
1228
+ status: 'passed' as const,
1229
+ note: `clean tracked marketplace remained pinned to ${actualRevision} with local and remote manifest version ${actualVersion}`
1230
+ }
1231
+ };
1232
+ } catch (error) {
1233
+ const note = boundedNote(errorMessage(error));
1234
+ return {
1235
+ ok: false,
1236
+ error: note,
1237
+ check: { id, status: 'failed' as const, note }
1238
+ };
1239
+ }
1240
+ }
1241
+
1024
1242
  async function installedInsaneVersion(
1025
1243
  deps: ResolvedDependencies,
1026
1244
  checks: ExternalComponentReceipt['checks'],
@@ -1069,6 +1287,7 @@ function baseReceipt(
1069
1287
  installed_version: check.installed_version,
1070
1288
  source: check.source,
1071
1289
  source_revision: receiptSourceRevision(check),
1290
+ source_receipt: check.source_receipt || null,
1072
1291
  checks,
1073
1292
  rollback_hint: rollbackHint,
1074
1293
  side_effects: [],
@@ -1408,6 +1627,45 @@ function requireStableVersion(value: unknown, label: string) {
1408
1627
  return version;
1409
1628
  }
1410
1629
 
1630
+ function requireIntegrityValue(value: unknown, label: string) {
1631
+ const integrity = String(value || '').trim();
1632
+ if (!/^sha(?:256|384|512)-[A-Za-z0-9+/=]+$/.test(integrity)) {
1633
+ throw new Error(`${label} is missing or malformed`);
1634
+ }
1635
+ return integrity;
1636
+ }
1637
+
1638
+ function requireGitRevision(value: unknown, label: string) {
1639
+ const revision = String(value || '').trim().toLowerCase();
1640
+ if (!/^[a-f0-9]{40}$/.test(revision)) throw new Error(`${label} is not a 40-character Git revision`);
1641
+ return revision;
1642
+ }
1643
+
1644
+ function pypiReleaseIntegrity(value: unknown, version: string) {
1645
+ if (!Array.isArray(value) || value.length === 0) {
1646
+ throw new Error(`Scrapling PyPI ${version} has no SHA-256 release artifact inventory`);
1647
+ }
1648
+ if (value.length > 64) {
1649
+ throw new Error(`Scrapling PyPI ${version} release artifact inventory exceeds the 64-item provenance limit`);
1650
+ }
1651
+ const artifacts = value.map((item, index) => {
1652
+ const filename = typeof item?.filename === 'string' ? item.filename.trim() : '';
1653
+ const rawSha256 = item?.digests?.sha256;
1654
+ const sha256 = typeof rawSha256 === 'string' ? rawSha256.trim().toLowerCase() : '';
1655
+ if (!filename || filename.length > 512 || /[\r\n\u0000-\u001f\u007f]/.test(filename)) {
1656
+ throw new Error(`Scrapling PyPI ${version} release artifact ${index} has a malformed filename`);
1657
+ }
1658
+ if (!/^[a-f0-9]{64}$/.test(sha256)) {
1659
+ throw new Error(`Scrapling PyPI ${version} release artifact ${index} has a missing or malformed SHA-256 digest`);
1660
+ }
1661
+ return `sha256:${sha256} filename:${filename}`;
1662
+ }).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
1663
+ if (new Set(artifacts).size !== artifacts.length) {
1664
+ throw new Error(`Scrapling PyPI ${version} release artifact inventory contains duplicates`);
1665
+ }
1666
+ return artifacts;
1667
+ }
1668
+
1411
1669
  function extractVersion(text: string) {
1412
1670
  return String(text || '').match(/\b\d+(?:\.\d+){1,3}\b/)?.[0] || '';
1413
1671
  }