yam-flow 2.4.0 → 2.5.1

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.
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import fsp from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { compareStableText } from './stable-order.js';
4
5
  export const INSTALL_RECEIPT_NAME = '.yam-flow-install-receipt.json';
5
6
  export const INSTALL_LOCK_NAME = '.yam-flow-install.lock';
6
7
  export const INSTALL_RECEIPT_SCHEMA = 'yam.install-receipt.v1';
@@ -70,7 +71,7 @@ async function collectTreeManifest(root, prefix = '') {
70
71
  const files = [];
71
72
  async function walk(current, relative) {
72
73
  const entries = await fsp.readdir(current, { withFileTypes: true });
73
- entries.sort((left, right) => left.name.localeCompare(right.name));
74
+ entries.sort((left, right) => compareStableText(left.name, right.name));
74
75
  for (const entry of entries) {
75
76
  const absolute = path.join(current, entry.name);
76
77
  const nextRelative = relative ? path.join(relative, entry.name) : entry.name;
@@ -86,7 +87,7 @@ async function collectTreeManifest(root, prefix = '') {
86
87
  }
87
88
  }
88
89
  await walk(root, '');
89
- return files.sort((left, right) => left.path.localeCompare(right.path));
90
+ return files.sort((left, right) => compareStableText(left.path, right.path));
90
91
  }
91
92
  async function expectedInstallManifest(sourceRoot, skills) {
92
93
  const referencesRoot = path.join(sourceRoot, 'references');
@@ -112,7 +113,7 @@ async function expectedInstallManifest(sourceRoot, skills) {
112
113
  });
113
114
  }
114
115
  }
115
- return files.sort((left, right) => left.path.localeCompare(right.path));
116
+ return files.sort((left, right) => compareStableText(left.path, right.path));
116
117
  }
117
118
  async function copyTree(source, target) {
118
119
  const stat = await fsp.lstat(source);
@@ -137,8 +138,11 @@ async function copyTree(source, target) {
137
138
  }
138
139
  }
139
140
  function manifestDigest(files) {
141
+ return digestManifestFiles([...files].sort((left, right) => compareStableText(left.path, right.path)));
142
+ }
143
+ function digestManifestFiles(files) {
140
144
  const digest = createHash('sha256');
141
- for (const file of [...files].sort((left, right) => left.path.localeCompare(right.path))) {
145
+ for (const file of files) {
142
146
  digest.update(file.path);
143
147
  digest.update('\0');
144
148
  digest.update(String(file.bytes));
@@ -148,6 +152,20 @@ function manifestDigest(files) {
148
152
  }
149
153
  return digest.digest('hex');
150
154
  }
155
+ function allowsLegacyManifestOrder(version) {
156
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(version || ''));
157
+ if (!match)
158
+ return false;
159
+ const major = Number(match[1]);
160
+ const minor = Number(match[2]);
161
+ const patch = Number(match[3]);
162
+ return major < 2 || (major === 2 && (minor < 5 || (minor === 5 && patch === 0)));
163
+ }
164
+ function receiptManifestDigestMatches(files, digest, version) {
165
+ if (digest === manifestDigest(files))
166
+ return true;
167
+ return allowsLegacyManifestOrder(version) && digest === digestManifestFiles(files);
168
+ }
151
169
  function manifestIssues(expected, actual, label) {
152
170
  const issues = [];
153
171
  const expectedByPath = new Map(expected.map((file) => [file.path, file]));
@@ -236,7 +254,7 @@ function ownershipReceiptIssues(receipt, packageName, destination) {
236
254
  if (value.integrity?.file_count !== files.length) {
237
255
  issues.push('install receipt file count does not match its manifest');
238
256
  }
239
- if (value.integrity?.source_digest !== manifestDigest(files)) {
257
+ if (!receiptManifestDigestMatches(files, value.integrity?.source_digest, value.package?.version)) {
240
258
  issues.push('install receipt source digest does not match its manifest');
241
259
  }
242
260
  if (Array.isArray(value.skills)) {
@@ -0,0 +1 @@
1
+ export declare function compareStableText(left: string, right: string): number;
@@ -0,0 +1,5 @@
1
+ export function compareStableText(left, right) {
2
+ if (left === right)
3
+ return 0;
4
+ return left < right ? -1 : 1;
5
+ }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { constants as fsConstants } from 'node:fs';
3
3
  import fsp from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { compareStableText } from './stable-order.js';
5
6
  export async function inspectImageFile(file) {
6
7
  const absolute = path.resolve(file);
7
8
  const buffer = await fsp.readFile(absolute);
@@ -46,7 +47,7 @@ export async function upsertUeyeAsset(input) {
46
47
  manifest.assets[existingIndex] = entry;
47
48
  else
48
49
  manifest.assets.push(entry);
49
- manifest.assets.sort((left, right) => left.id.localeCompare(right.id));
50
+ manifest.assets.sort((left, right) => compareStableText(left.id, right.id));
50
51
  manifest.updated_at = new Date().toISOString();
51
52
  await writeJsonAtomic(manifestPath, manifest);
52
53
  const warnings = assetWarnings(entry);
@@ -152,7 +153,7 @@ export async function archiveUeyeRevision(input) {
152
153
  else {
153
154
  history.revisions.push(entry);
154
155
  }
155
- history.revisions.sort((left, right) => left.round - right.round || left.artifact_id.localeCompare(right.artifact_id));
156
+ history.revisions.sort((left, right) => left.round - right.round || compareStableText(left.artifact_id, right.artifact_id));
156
157
  history.updated_at = new Date().toISOString();
157
158
  await writeJsonAtomic(manifestPath, history);
158
159
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yam-flow",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Progressive proof-first agent flow: start fast, deepen deliberately, stay honest by design.",
5
5
  "type": "module",
6
6
  "author": "0kim0bos",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "scripts": {
45
45
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
46
- "build": "npm run clean && tsc -p tsconfig.json",
46
+ "build": "npm run clean && tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/bin/yam.js',0o755)\"",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
48
  "install:skills": "node ./dist/bin/yam.js install",
49
49
  "uninstall:skills": "node ./dist/bin/yam.js uninstall",
@@ -52,7 +52,7 @@
52
52
  "doctor": "node ./dist/bin/yam.js doctor",
53
53
  "list": "node ./dist/bin/yam.js list",
54
54
  "examples": "node ./dist/bin/yam.js examples",
55
- "verify:self": "npm run build && node --check ./dist/bin/yam.js && node --check ./dist/lib/trust-kernel.js && node --check ./dist/lib/skill-installation.js && npm run hook-latency-smoke && node ./scripts/install-transaction-smoke.mjs && node ./dist/bin/yam.js verify && node ./dist/bin/yam.js doctor",
55
+ "verify:self": "npm run build && node --check ./dist/bin/yam.js && node --check ./dist/lib/trust-kernel.js && node --check ./dist/lib/skill-installation.js && node --check ./dist/lib/external-updates.js && npm run hook-latency-smoke && node ./scripts/external-updates-smoke.mjs && node ./scripts/install-transaction-smoke.mjs && npm run package-secrets:check && npm run package-secrets-smoke && node ./dist/bin/yam.js verify && node ./dist/bin/yam.js doctor",
56
56
  "prepare": "npm run build",
57
57
  "prepack": "npm run verify:self",
58
58
  "prepublishOnly": "npm run verify:self",
@@ -61,9 +61,12 @@
61
61
  "forbidden-names:check": "node ./scripts/check-forbidden-names.mjs",
62
62
  "study-note:check": "node ./dist/bin/yam.js study-note check --json",
63
63
  "package-boundary:check": "node ./scripts/check-package-boundary.mjs",
64
+ "package-secrets:check": "node ./scripts/check-package-secrets.mjs",
65
+ "package-secrets-smoke": "node ./scripts/package-secrets-smoke.mjs",
64
66
  "dist:freshness": "node ./scripts/check-dist-freshness.mjs",
65
67
  "registry:check": "node ./scripts/check-registry-version.mjs",
66
68
  "cli-smoke": "node ./scripts/cli-smoke.mjs",
69
+ "external-updates-smoke": "npm run build && node ./scripts/external-updates-smoke.mjs",
67
70
  "hook-latency-smoke": "node ./scripts/hook-latency-smoke.mjs",
68
71
  "install-transaction-smoke": "npm run build && node ./scripts/install-transaction-smoke.mjs",
69
72
  "release:check": "npm run typecheck && npm run forbidden-names:check && npm run verify:self && npm run dist:freshness && npm run package-boundary:check && npm run registry:check && npm run cli-smoke",
package/src/bin/yam.ts CHANGED
@@ -39,6 +39,19 @@ import {
39
39
  installSkillSetTransactional,
40
40
  uninstallSkillSetSafely
41
41
  } from '../lib/skill-installation.js';
42
+ import {
43
+ EXTERNAL_UPDATE_COMPONENTS,
44
+ applyExternalUpdates,
45
+ checkExternalUpdates
46
+ } from '../lib/external-updates.js';
47
+ import type { ExternalUpdateComponent } from '../lib/external-updates.js';
48
+ import {
49
+ BoundedInputError,
50
+ GENERAL_STDIN_MAX_BYTES,
51
+ HOOK_STDIN_MAX_BYTES,
52
+ readBoundedStdinJson,
53
+ readBoundedStdinText
54
+ } from '../lib/bounded-input.js';
42
55
 
43
56
  type AnyRecord = Record<string, any>;
44
57
 
@@ -180,6 +193,9 @@ Usage:
180
193
  yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
181
194
  yam mission receipt [--thread-id id] [--role reviewer] [--lifecycle stopped] [--outcome passed] [--evidence text] [--json]
182
195
  yam mission gate [--expected-thread id] [--receipt file] [--json]
196
+ yam update check [--json]
197
+ yam update apply --component yam|scrapling|insane-search [--json]
198
+ yam update apply --all [--json]
183
199
  yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
184
200
  yam release report [--json]
185
201
  yam safety [text...]
@@ -2013,7 +2029,18 @@ async function hookRun(args = []) {
2013
2029
  console.log(JSON.stringify({ continue: true }));
2014
2030
  return;
2015
2031
  }
2016
- const input = await readStdinJson();
2032
+ let input: AnyRecord;
2033
+ try {
2034
+ input = await readStdinJson();
2035
+ } catch (error) {
2036
+ if (!(error instanceof BoundedInputError)) throw error;
2037
+ const code = error.code === 'invalid_json' ? 'invalid_hook_json' : error.code;
2038
+ console.log(JSON.stringify({
2039
+ continue: true,
2040
+ systemMessage: `yam-${profile} hook input rejected: ${code}; continuing without using or echoing the payload.`
2041
+ }));
2042
+ return;
2043
+ }
2017
2044
  const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
2018
2045
  const cwd = String(input?.cwd || process.cwd());
2019
2046
  if (profile === 'study-note' && event === 'Stop') {
@@ -2035,15 +2062,7 @@ async function hookRun(args = []) {
2035
2062
  }
2036
2063
 
2037
2064
  async function readStdinJson() {
2038
- const chunks = [];
2039
- for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
2040
- const text = Buffer.concat(chunks).toString('utf8').trim();
2041
- if (!text) return {};
2042
- try {
2043
- return JSON.parse(text);
2044
- } catch {
2045
- return {};
2046
- }
2065
+ return readBoundedStdinJson(process.stdin, HOOK_STDIN_MAX_BYTES);
2047
2066
  }
2048
2067
 
2049
2068
  function extractPrompt(input: AnyRecord = {}) {
@@ -2676,9 +2695,7 @@ function redactSensitiveText(text = '') {
2676
2695
 
2677
2696
  async function readStdinTextIfAvailable() {
2678
2697
  if (process.stdin.isTTY) return '';
2679
- const chunks = [];
2680
- for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
2681
- return Buffer.concat(chunks).toString('utf8').trim();
2698
+ return (await readBoundedStdinText(process.stdin, GENERAL_STDIN_MAX_BYTES)).trim();
2682
2699
  }
2683
2700
 
2684
2701
  async function loop(args = []) {
@@ -3564,6 +3581,83 @@ async function mission(args = []) {
3564
3581
  return missionUsage();
3565
3582
  }
3566
3583
 
3584
+ async function updateCommand(args: string[] = []) {
3585
+ const subcommand = args[0] || 'help';
3586
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return updateUsage();
3587
+ if (subcommand === 'check') {
3588
+ const rest = args.slice(1);
3589
+ const invalid = rest.filter((arg) => arg !== '--json');
3590
+ if (invalid.length || rest.filter((arg) => arg === '--json').length > 1) {
3591
+ throw new Error(`invalid update check option: ${invalid[0] || '--json repeated'}`);
3592
+ }
3593
+ const report = await checkExternalUpdates(VERSION);
3594
+ printJsonOrHuman(report, rest.includes('--json'), 'External update check');
3595
+ if (!report.success) process.exitCode = 1;
3596
+ return;
3597
+ }
3598
+ if (subcommand !== 'apply') {
3599
+ throw new Error(`unknown update command: ${subcommand}`);
3600
+ }
3601
+
3602
+ const parsed = parseUpdateApplyArgs(args.slice(1));
3603
+ const report = await applyExternalUpdates(VERSION, {
3604
+ component: parsed.component,
3605
+ all: parsed.all
3606
+ });
3607
+ printJsonOrHuman(report, parsed.json, 'External update apply');
3608
+ if (!report.success) process.exitCode = 1;
3609
+ }
3610
+
3611
+ function parseUpdateApplyArgs(args: string[]) {
3612
+ let component: ExternalUpdateComponent | undefined;
3613
+ let all = false;
3614
+ let json = false;
3615
+ for (let index = 0; index < args.length; index += 1) {
3616
+ const arg = String(args[index] || '');
3617
+ if (arg === '--json') {
3618
+ if (json) throw new Error('--json may be supplied only once');
3619
+ json = true;
3620
+ continue;
3621
+ }
3622
+ if (arg === '--all') {
3623
+ if (all) throw new Error('--all may be supplied only once');
3624
+ all = true;
3625
+ continue;
3626
+ }
3627
+ if (arg === '--component' || arg.startsWith('--component=')) {
3628
+ if (component) throw new Error('--component may be supplied only once');
3629
+ const value = arg === '--component' ? String(args[index + 1] || '') : arg.slice('--component='.length);
3630
+ if (arg === '--component') index += 1;
3631
+ if (!EXTERNAL_UPDATE_COMPONENTS.includes(value as ExternalUpdateComponent)) {
3632
+ throw new Error('component must be one of: yam, scrapling, insane-search');
3633
+ }
3634
+ component = value as ExternalUpdateComponent;
3635
+ continue;
3636
+ }
3637
+ throw new Error(`invalid update apply option: ${arg}`);
3638
+ }
3639
+ if ((all && component) || (!all && !component)) {
3640
+ throw new Error('choose exactly one of --component yam|scrapling|insane-search or --all');
3641
+ }
3642
+ return { component, all, json };
3643
+ }
3644
+
3645
+ function updateUsage() {
3646
+ console.log(`yam update
3647
+
3648
+ Usage:
3649
+ yam update check [--json]
3650
+ yam update apply --component yam|scrapling|insane-search [--json]
3651
+ yam update apply --all [--json]
3652
+
3653
+ Safety:
3654
+ check is read-only. apply requires an explicit component or --all.
3655
+ --all updates Scrapling and Insane Search before yam, then stops at the first failed/manual component.
3656
+ Insane Search is updated only through the official Codex CLI; plugin caches are never edited directly.
3657
+ Every apply attempt records component checks, outcome, side effects, and rollback guidance.
3658
+ `);
3659
+ }
3660
+
3567
3661
  function missionUsage() {
3568
3662
  console.log(`yam mission
3569
3663
 
@@ -5332,6 +5426,7 @@ function showRequestedHelp(args: string[]) {
5332
5426
  media: mediaUsage,
5333
5427
  runtime: runtimeUsage,
5334
5428
  mission: missionUsage,
5429
+ update: updateUsage,
5335
5430
  benchmark: benchmarkUsage,
5336
5431
  memory: memoryUsage
5337
5432
  };
@@ -5361,6 +5456,7 @@ async function main() {
5361
5456
  if (command === 'media') return media(process.argv.slice(3));
5362
5457
  if (command === 'runtime') return runtime(process.argv.slice(3));
5363
5458
  if (command === 'mission') return mission(process.argv.slice(3));
5459
+ if (command === 'update') return updateCommand(process.argv.slice(3));
5364
5460
  if (command === 'benchmark') return benchmark(process.argv.slice(3));
5365
5461
  if (command === 'release') return release(process.argv.slice(3));
5366
5462
  if (command === 'safety') return safety(process.argv.slice(3));
@@ -0,0 +1,59 @@
1
+ export const HOOK_STDIN_MAX_BYTES = 1024 * 1024;
2
+ export const GENERAL_STDIN_MAX_BYTES = 4 * 1024 * 1024;
3
+
4
+ export type BoundedInputErrorCode = 'input_too_large' | 'invalid_json';
5
+
6
+ export class BoundedInputError extends Error {
7
+ readonly code: BoundedInputErrorCode;
8
+ readonly maxBytes?: number;
9
+
10
+ constructor(code: BoundedInputErrorCode, options: { maxBytes?: number } = {}) {
11
+ const detail = code === 'input_too_large' && options.maxBytes
12
+ ? `stdin input exceeds the ${options.maxBytes}-byte limit`
13
+ : 'stdin input is not a valid JSON object';
14
+ super(`[${code}] ${detail}`);
15
+ this.name = 'BoundedInputError';
16
+ this.code = code;
17
+ this.maxBytes = options.maxBytes;
18
+ }
19
+ }
20
+
21
+ export async function readBoundedStdinText(
22
+ input: NodeJS.ReadableStream & AsyncIterable<string | Buffer>,
23
+ maxBytes: number
24
+ ): Promise<string> {
25
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
26
+ throw new Error('stdin byte limit must be a positive safe integer');
27
+ }
28
+
29
+ const chunks: Buffer[] = [];
30
+ let totalBytes = 0;
31
+ for await (const chunk of input) {
32
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
33
+ totalBytes += bytes.byteLength;
34
+ if (totalBytes > maxBytes) {
35
+ throw new BoundedInputError('input_too_large', { maxBytes });
36
+ }
37
+ chunks.push(bytes);
38
+ }
39
+ return Buffer.concat(chunks, totalBytes).toString('utf8');
40
+ }
41
+
42
+ export async function readBoundedStdinJson(
43
+ input: NodeJS.ReadableStream & AsyncIterable<string | Buffer>,
44
+ maxBytes: number
45
+ ): Promise<Record<string, unknown>> {
46
+ const text = (await readBoundedStdinText(input, maxBytes)).trim();
47
+ if (!text) return {};
48
+
49
+ try {
50
+ const value = JSON.parse(text);
51
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
52
+ throw new BoundedInputError('invalid_json');
53
+ }
54
+ return value;
55
+ } catch (error) {
56
+ if (error instanceof BoundedInputError) throw error;
57
+ throw new BoundedInputError('invalid_json');
58
+ }
59
+ }