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
@@ -41,6 +41,14 @@ export interface ExternalComponentCheck {
41
41
  drift: boolean;
42
42
  note: string;
43
43
  };
44
+ source_receipt?: {
45
+ canonical_url: string;
46
+ retrieved_at: string;
47
+ access_path: string;
48
+ version: string;
49
+ revision: string;
50
+ integrity: string[];
51
+ };
44
52
  executable?: string;
45
53
  error?: string;
46
54
  }
@@ -54,6 +62,7 @@ export interface ExternalComponentReceipt {
54
62
  installed_version: string;
55
63
  source: string;
56
64
  source_revision: JsonRecord | null;
65
+ source_receipt: JsonRecord | null;
57
66
  checks: Array<{
58
67
  id: string;
59
68
  status: 'passed' | 'failed' | 'skipped';
@@ -6,7 +6,9 @@ import path from 'node:path';
6
6
  import { buildStrictGateResult, verifyStrictGateResult } from './gate-result.js';
7
7
  export const EXTERNAL_UPDATE_COMPONENTS = ['yam', 'scrapling', 'insane-search'];
8
8
  const YAM_REGISTRY_URL = 'https://registry.npmjs.org/yam-flow/latest';
9
+ const NPM_REGISTRY = 'https://registry.npmjs.org/';
9
10
  const SCRAPLING_PYPI_URL = 'https://pypi.org/pypi/scrapling/json';
11
+ const SCRAPLING_PYPI_INDEX = 'https://pypi.org/simple';
10
12
  const INSANE_MARKETPLACE = 'gptaku-codex';
11
13
  const INSANE_PLUGIN_ID = 'insane-search-codex@gptaku-codex';
12
14
  const INSANE_MARKETPLACE_SOURCE = 'https://github.com/fivetaku/gptaku-plugins-codex.git';
@@ -269,7 +271,19 @@ async function checkYam(currentYamVersion, deps) {
269
271
  const data = await deps.fetchJson(YAM_REGISTRY_URL);
270
272
  const latest = requireStableVersion(data?.version, 'yam registry version');
271
273
  const installed = requireStableVersion(currentYamVersion, 'installed yam version');
272
- return componentCheck('yam', installed, latest, 'npm:yam-flow');
274
+ const integrity = requireIntegrityValue(data?.dist?.integrity, 'yam registry integrity');
275
+ const revision = requireGitRevision(data?.gitHead, 'yam registry gitHead');
276
+ return {
277
+ ...componentCheck('yam', installed, latest, 'npm:yam-flow'),
278
+ source_receipt: {
279
+ canonical_url: YAM_REGISTRY_URL,
280
+ retrieved_at: deps.now().toISOString(),
281
+ access_path: 'npm registry latest endpoint',
282
+ version: latest,
283
+ revision,
284
+ integrity: [integrity]
285
+ }
286
+ };
273
287
  }
274
288
  catch (error) {
275
289
  return failedCheck('yam', currentYamVersion, 'npm:yam-flow', error);
@@ -279,6 +293,7 @@ async function checkScrapling(deps) {
279
293
  try {
280
294
  const data = await deps.fetchJson(SCRAPLING_PYPI_URL);
281
295
  const latest = requireStableVersion(data?.info?.version, 'Scrapling PyPI version');
296
+ const releaseIntegrity = pypiReleaseIntegrity(data?.urls, latest);
282
297
  const executable = deps.paths.scraplingBin;
283
298
  const versionResult = await deps.run(executable, ['--version'], { env: deps.env, timeout: 30000 });
284
299
  if (!versionResult.ok) {
@@ -289,12 +304,28 @@ async function checkScrapling(deps) {
289
304
  update_available: true,
290
305
  status: 'not_installed',
291
306
  source: 'pypi:scrapling',
307
+ source_receipt: {
308
+ canonical_url: SCRAPLING_PYPI_URL,
309
+ retrieved_at: deps.now().toISOString(),
310
+ access_path: 'PyPI project JSON endpoint',
311
+ version: latest,
312
+ revision: '',
313
+ integrity: releaseIntegrity
314
+ },
292
315
  executable
293
316
  };
294
317
  }
295
318
  const installed = requireStableVersion(extractVersion(versionResult.stdout), 'installed Scrapling version');
296
319
  return {
297
320
  ...componentCheck('scrapling', installed, latest, 'pypi:scrapling'),
321
+ source_receipt: {
322
+ canonical_url: SCRAPLING_PYPI_URL,
323
+ retrieved_at: deps.now().toISOString(),
324
+ access_path: 'PyPI project JSON endpoint',
325
+ version: latest,
326
+ revision: '',
327
+ integrity: releaseIntegrity
328
+ },
298
329
  executable
299
330
  };
300
331
  }
@@ -376,6 +407,14 @@ async function checkInsaneSearch(deps) {
376
407
  note: installed && compareVersions(installed, latest) === 0
377
408
  ? 'marketplace revision drift alone is not treated as an Insane Search update'
378
409
  : 'plugin update availability is based on the official plugin manifest version'
410
+ },
411
+ source_receipt: {
412
+ canonical_url: manifestUrl,
413
+ retrieved_at: deps.now().toISOString(),
414
+ access_path: 'Git-pinned raw marketplace manifest',
415
+ version: latest,
416
+ revision: remoteRevision,
417
+ integrity: [`git_commit:${remoteRevision.toLowerCase()}`]
379
418
  }
380
419
  };
381
420
  }
@@ -419,7 +458,13 @@ async function applyYam(currentYamVersion, deps) {
419
458
  const checks = [];
420
459
  const sideEffects = [];
421
460
  const exactPackage = `yam-flow@${check.latest_version}`;
422
- const install = await deps.run('npm', ['install', '-g', exactPackage], {
461
+ const install = await deps.run('npm', [
462
+ 'install',
463
+ '-g',
464
+ exactPackage,
465
+ '--registry',
466
+ NPM_REGISTRY
467
+ ], {
423
468
  env: deps.env,
424
469
  timeout: 600000
425
470
  });
@@ -495,7 +540,13 @@ async function applyYam(currentYamVersion, deps) {
495
540
  async function rollbackYam(previousVersion, deps, checks) {
496
541
  if (!previousVersion)
497
542
  return { ok: false };
498
- const result = await deps.run('npm', ['install', '-g', `yam-flow@${previousVersion}`], {
543
+ const result = await deps.run('npm', [
544
+ 'install',
545
+ '-g',
546
+ `yam-flow@${previousVersion}`,
547
+ '--registry',
548
+ NPM_REGISTRY
549
+ ], {
499
550
  env: deps.env,
500
551
  timeout: 600000
501
552
  });
@@ -661,7 +712,15 @@ async function applyScrapling(deps) {
661
712
  const candidatePython = path.join(candidate, 'bin', 'python');
662
713
  const candidateScrapling = path.join(candidate, 'bin', 'scrapling');
663
714
  const packageSpec = `scrapling[fetchers]==${check.latest_version}`;
664
- const install = await deps.run(candidatePython, ['-m', 'pip', 'install', packageSpec], {
715
+ const install = await deps.run(candidatePython, [
716
+ '-m',
717
+ 'pip',
718
+ 'install',
719
+ '--isolated',
720
+ '--index-url',
721
+ SCRAPLING_PYPI_INDEX,
722
+ packageSpec
723
+ ], {
665
724
  env: deps.env,
666
725
  timeout: 600000
667
726
  });
@@ -797,6 +856,15 @@ async function applyInsaneSearch(deps) {
797
856
  error: `marketplace upgrade failed: ${commandNote(upgrade)}`
798
857
  }), deps);
799
858
  }
859
+ const pinnedSource = await verifyUpgradedInsaneSource(check, deps, 'pre_add_source_revision');
860
+ checks.push(pinnedSource.check);
861
+ if (!pinnedSource.ok) {
862
+ return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
863
+ installed_version: check.installed_version,
864
+ side_effects: ['The official Codex marketplace snapshot was upgraded; plugin add was blocked because the post-upgrade source did not match the checked revision.'],
865
+ error: pinnedSource.error
866
+ }), deps);
867
+ }
800
868
  let observation = await installedInsaneVersion(deps, checks, 'plugin_list_after_marketplace_upgrade');
801
869
  if (!observation.ok) {
802
870
  return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
@@ -836,6 +904,20 @@ async function applyInsaneSearch(deps) {
836
904
  error: `official CLI finished without installing expected version ${check.latest_version}`
837
905
  }), deps);
838
906
  }
907
+ const finalPinnedSource = await verifyUpgradedInsaneSource(check, deps, 'final_source_revision');
908
+ checks.push(finalPinnedSource.check);
909
+ if (!finalPinnedSource.ok) {
910
+ return persistReceipt(baseReceipt(check, 'failed', checks, insaneRollback(check.installed_version), {
911
+ installed_version: installedAfter,
912
+ side_effects: [
913
+ `upgraded official marketplace ${INSANE_MARKETPLACE}`,
914
+ installedAfter === check.latest_version
915
+ ? `the plugin reached ${installedAfter}, but final source provenance changed and completion was blocked`
916
+ : 'plugin state did not reach the checked version'
917
+ ],
918
+ error: finalPinnedSource.error
919
+ }), deps);
920
+ }
839
921
  return persistReceipt(baseReceipt(check, 'updated', checks, insaneRollback(check.installed_version), {
840
922
  installed_version: installedAfter,
841
923
  side_effects: [
@@ -845,6 +927,114 @@ async function applyInsaneSearch(deps) {
845
927
  truth_status: 'verified'
846
928
  }), deps);
847
929
  }
930
+ async function verifyUpgradedInsaneSource(check, deps, id) {
931
+ try {
932
+ const expectedRevision = requireGitRevision(check.source_receipt?.revision, 'checked Insane Search source revision');
933
+ const marketplaceList = await deps.run('codex', ['plugin', 'marketplace', 'list', '--json'], {
934
+ env: deps.env,
935
+ timeout: 30000
936
+ });
937
+ if (!marketplaceList.ok) {
938
+ throw new Error(`post-upgrade marketplace state could not be observed: ${commandNote(marketplaceList)}`);
939
+ }
940
+ const parsedMarketplaces = parseJsonOutput(marketplaceList.stdout, 'post-upgrade codex plugin marketplace list');
941
+ const marketplaceEntry = arrayValue(parsedMarketplaces?.marketplaces).find((item) => item?.name === INSANE_MARKETPLACE);
942
+ if (!marketplaceEntry)
943
+ throw new Error(`post-upgrade marketplace is missing: ${INSANE_MARKETPLACE}`);
944
+ const configuredSource = String(marketplaceEntry?.marketplaceSource?.source || '');
945
+ if (marketplaceEntry?.marketplaceSource?.sourceType !== 'git'
946
+ || configuredSource !== INSANE_MARKETPLACE_SOURCE) {
947
+ throw new Error(`refusing unexpected post-upgrade ${INSANE_MARKETPLACE} marketplace source`);
948
+ }
949
+ const marketplaceRoot = String(marketplaceEntry.root || '');
950
+ if (!marketplaceRoot || !isSafeMarketplaceRoot(marketplaceRoot, deps.homeDir)) {
951
+ throw new Error(`refusing unsafe post-upgrade ${INSANE_MARKETPLACE} marketplace root`);
952
+ }
953
+ const localRevisionResult = await deps.run('git', ['-C', marketplaceRoot, 'rev-parse', 'HEAD'], {
954
+ env: deps.env,
955
+ timeout: 30000
956
+ });
957
+ if (!localRevisionResult.ok) {
958
+ throw new Error(`post-upgrade marketplace revision could not be read: ${commandNote(localRevisionResult)}`);
959
+ }
960
+ const actualRevision = requireGitRevision(localRevisionResult.stdout.trim(), 'post-upgrade Insane Search marketplace revision');
961
+ const statusResult = await deps.run('git', [
962
+ '-C',
963
+ marketplaceRoot,
964
+ 'status',
965
+ '--porcelain=v1',
966
+ '--untracked-files=all',
967
+ '--ignored=matching'
968
+ ], { env: deps.env, timeout: 30000 });
969
+ if (!statusResult.ok) {
970
+ throw new Error(`post-upgrade marketplace cleanliness could not be observed: ${commandNote(statusResult)}`);
971
+ }
972
+ if (statusResult.stdout.trim()) {
973
+ throw new Error('post-upgrade Insane Search marketplace working tree is dirty; refusing to bind plugin install to a clean commit receipt');
974
+ }
975
+ const trackedManifest = await deps.run('git', [
976
+ '-C',
977
+ marketplaceRoot,
978
+ 'ls-files',
979
+ '--error-unmatch',
980
+ '--',
981
+ INSANE_MANIFEST_PATH
982
+ ], { env: deps.env, timeout: 30000 });
983
+ if (!trackedManifest.ok) {
984
+ throw new Error(`local Insane Search manifest is not tracked at the checked commit: ${commandNote(trackedManifest)}`);
985
+ }
986
+ const localManifestPath = path.join(marketplaceRoot, INSANE_MANIFEST_PATH);
987
+ assertChildPath(marketplaceRoot, localManifestPath, 'local Insane Search manifest');
988
+ const localManifestStat = await fsp.lstat(localManifestPath);
989
+ if (!localManifestStat.isFile() || localManifestStat.isSymbolicLink() || localManifestStat.size > 1024 * 1024) {
990
+ throw new Error('local Insane Search manifest must be a regular tracked file no larger than 1 MiB');
991
+ }
992
+ const localManifestBytes = await fsp.readFile(localManifestPath, 'utf8');
993
+ const committedManifest = await deps.run('git', [
994
+ '-C',
995
+ marketplaceRoot,
996
+ 'show',
997
+ `${actualRevision}:${INSANE_MANIFEST_PATH}`
998
+ ], { env: deps.env, timeout: 30000 });
999
+ if (!committedManifest.ok) {
1000
+ throw new Error(`checked Insane Search manifest bytes could not be read from Git: ${commandNote(committedManifest)}`);
1001
+ }
1002
+ if (localManifestBytes !== committedManifest.stdout) {
1003
+ throw new Error('local Insane Search manifest bytes do not match the checked Git commit');
1004
+ }
1005
+ const localManifest = parseJsonOutput(localManifestBytes, 'local Insane Search manifest');
1006
+ const localVersion = requireStableVersion(localManifest?.version, 'local Insane Search manifest version');
1007
+ const manifestUrl = `${INSANE_MANIFEST_ROOT}/${actualRevision}/${INSANE_MANIFEST_PATH}`;
1008
+ const manifest = await deps.fetchJson(manifestUrl);
1009
+ const actualVersion = requireStableVersion(manifest?.version, 'post-upgrade Insane Search manifest version');
1010
+ if (actualRevision !== expectedRevision) {
1011
+ throw new Error(`Insane Search source changed between check and upgrade: checked ${expectedRevision}, observed ${actualRevision}; rerun the read-only check before retrying`);
1012
+ }
1013
+ if (actualVersion !== check.latest_version) {
1014
+ throw new Error(`post-upgrade Insane Search manifest version ${actualVersion} does not match checked ${check.latest_version}`);
1015
+ }
1016
+ if (localVersion !== actualVersion) {
1017
+ throw new Error(`local Insane Search manifest version ${localVersion} does not match pinned source version ${actualVersion}`);
1018
+ }
1019
+ return {
1020
+ ok: true,
1021
+ error: '',
1022
+ check: {
1023
+ id,
1024
+ status: 'passed',
1025
+ note: `clean tracked marketplace remained pinned to ${actualRevision} with local and remote manifest version ${actualVersion}`
1026
+ }
1027
+ };
1028
+ }
1029
+ catch (error) {
1030
+ const note = boundedNote(errorMessage(error));
1031
+ return {
1032
+ ok: false,
1033
+ error: note,
1034
+ check: { id, status: 'failed', note }
1035
+ };
1036
+ }
1037
+ }
848
1038
  async function installedInsaneVersion(deps, checks, id) {
849
1039
  const result = await deps.run('codex', ['plugin', 'list', '--json'], { env: deps.env, timeout: 30000 });
850
1040
  checks.push(commandCheck(id, result));
@@ -883,6 +1073,7 @@ function baseReceipt(check, outcome, checks, rollbackHint, overrides = {}) {
883
1073
  installed_version: check.installed_version,
884
1074
  source: check.source,
885
1075
  source_revision: receiptSourceRevision(check),
1076
+ source_receipt: check.source_receipt || null,
886
1077
  checks,
887
1078
  rollback_hint: rollbackHint,
888
1079
  side_effects: [],
@@ -1204,6 +1395,43 @@ function requireStableVersion(value, label) {
1204
1395
  }
1205
1396
  return version;
1206
1397
  }
1398
+ function requireIntegrityValue(value, label) {
1399
+ const integrity = String(value || '').trim();
1400
+ if (!/^sha(?:256|384|512)-[A-Za-z0-9+/=]+$/.test(integrity)) {
1401
+ throw new Error(`${label} is missing or malformed`);
1402
+ }
1403
+ return integrity;
1404
+ }
1405
+ function requireGitRevision(value, label) {
1406
+ const revision = String(value || '').trim().toLowerCase();
1407
+ if (!/^[a-f0-9]{40}$/.test(revision))
1408
+ throw new Error(`${label} is not a 40-character Git revision`);
1409
+ return revision;
1410
+ }
1411
+ function pypiReleaseIntegrity(value, version) {
1412
+ if (!Array.isArray(value) || value.length === 0) {
1413
+ throw new Error(`Scrapling PyPI ${version} has no SHA-256 release artifact inventory`);
1414
+ }
1415
+ if (value.length > 64) {
1416
+ throw new Error(`Scrapling PyPI ${version} release artifact inventory exceeds the 64-item provenance limit`);
1417
+ }
1418
+ const artifacts = value.map((item, index) => {
1419
+ const filename = typeof item?.filename === 'string' ? item.filename.trim() : '';
1420
+ const rawSha256 = item?.digests?.sha256;
1421
+ const sha256 = typeof rawSha256 === 'string' ? rawSha256.trim().toLowerCase() : '';
1422
+ if (!filename || filename.length > 512 || /[\r\n\u0000-\u001f\u007f]/.test(filename)) {
1423
+ throw new Error(`Scrapling PyPI ${version} release artifact ${index} has a malformed filename`);
1424
+ }
1425
+ if (!/^[a-f0-9]{64}$/.test(sha256)) {
1426
+ throw new Error(`Scrapling PyPI ${version} release artifact ${index} has a missing or malformed SHA-256 digest`);
1427
+ }
1428
+ return `sha256:${sha256} filename:${filename}`;
1429
+ }).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
1430
+ if (new Set(artifacts).size !== artifacts.length) {
1431
+ throw new Error(`Scrapling PyPI ${version} release artifact inventory contains duplicates`);
1432
+ }
1433
+ return artifacts;
1434
+ }
1207
1435
  function extractVersion(text) {
1208
1436
  return String(text || '').match(/\b\d+(?:\.\d+){1,3}\b/)?.[0] || '';
1209
1437
  }
@@ -0,0 +1,61 @@
1
+ export type NextStepKind = 'fix_first' | 'planned';
2
+ export type NextStepEvidenceLevel = 'L0' | 'L1' | 'L2' | 'L3' | 'L4' | 'L5';
3
+ export type NextStepTruthStatus = 'proven' | 'verified' | 'partial' | 'fixture_only' | 'fixture_instrumented_real' | 'integration_optional' | 'real_required_missing' | 'skipped' | 'blocked' | 'assumed';
4
+ export type NextStepOwnerRoute = '$quick' | '$ueye' | '$question' | '$scout' | '$deep' | '$mission';
5
+ export interface NextStepItemInput {
6
+ kind?: NextStepKind;
7
+ action?: string;
8
+ why?: string;
9
+ owner_route?: string;
10
+ owner_scope?: string[];
11
+ blocked_by?: string[];
12
+ safe_retry?: string;
13
+ side_effects?: string[];
14
+ }
15
+ export interface NextStepInput {
16
+ current_situation?: string;
17
+ forward_outlook?: string;
18
+ critical_opinion?: string;
19
+ improvement_recommendations?: string[];
20
+ steps?: NextStepItemInput[];
21
+ evidence_level?: NextStepEvidenceLevel;
22
+ evidence_stamp?: string;
23
+ truth_status?: NextStepTruthStatus;
24
+ }
25
+ export interface NextStepReceipt {
26
+ schema: 'yam.next-step.v1';
27
+ scan: {
28
+ current_situation: string;
29
+ forward_outlook: string;
30
+ critical_opinion: string;
31
+ improvement_recommendations: string[];
32
+ };
33
+ steps: Array<{
34
+ order: number;
35
+ kind: NextStepKind;
36
+ action: string;
37
+ why: string;
38
+ owner_route: NextStepOwnerRoute;
39
+ owner_scope: string[];
40
+ blocked_by: string[];
41
+ safe_retry: string;
42
+ side_effects: string[];
43
+ }>;
44
+ evidence: {
45
+ level: NextStepEvidenceLevel;
46
+ stamp: string;
47
+ };
48
+ contract_valid: boolean;
49
+ contract_errors: string[];
50
+ truth_status: NextStepTruthStatus;
51
+ digest: string;
52
+ }
53
+ export declare function buildNextStep(input?: NextStepInput): NextStepReceipt;
54
+ export declare function verifyNextStep(value: unknown): {
55
+ schema: "yam.next-step-verification.v1";
56
+ valid: boolean;
57
+ errors: string[];
58
+ receipt_truth_status: NextStepTruthStatus;
59
+ truth_status: "verified" | "blocked";
60
+ next_action: string;
61
+ };