yam-flow 2.8.1 → 2.8.3
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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +25 -0
- package/COMMANDS.md +7 -2
- package/README.md +12 -3
- package/dist/bin/yam.js +171 -76
- package/dist/lib/design-production.d.ts +6 -6
- package/dist/lib/external-updates.d.ts +6 -1
- package/dist/lib/external-updates.js +93 -6
- package/dist/lib/hook-readiness.d.ts +54 -0
- package/dist/lib/hook-readiness.js +259 -0
- package/dist/lib/memory-records.d.ts +29 -0
- package/dist/lib/memory-records.js +92 -0
- package/dist/lib/study-note-scope.d.ts +8 -0
- package/dist/lib/study-note-scope.js +165 -0
- package/dist/lib/verification-closure.d.ts +1 -1
- package/package.json +8 -3
- package/references/context-reuse.md +1 -0
- package/references/final-report.md +3 -1
- package/references/next-step.md +2 -0
- package/references/security-check.md +2 -0
- package/references/study-note.md +4 -0
- package/skills/deep/SKILL.md +1 -0
- package/skills/mission/SKILL.md +1 -0
- package/skills/question/SKILL.md +1 -0
- package/skills/quick/SKILL.md +2 -1
- package/skills/scout/SKILL.md +1 -0
- package/skills/ueye/SKILL.md +1 -0
- package/src/bin/yam.ts +151 -74
- package/src/lib/external-updates.ts +87 -6
- package/src/lib/hook-readiness.ts +215 -0
- package/src/lib/memory-records.ts +109 -0
- package/src/lib/study-note-scope.ts +134 -0
- package/yam.manifest.json +4 -3
package/src/bin/yam.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { studyNoteTurnScope } from '../lib/study-note-scope.js';
|
|
4
|
+
import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
|
|
5
|
+
import { readMemoryDirectory } from '../lib/memory-records.js';
|
|
3
6
|
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
4
7
|
import { createHash } from 'node:crypto';
|
|
5
8
|
import fs from 'node:fs';
|
|
@@ -149,8 +152,8 @@ const INSTRUCTION_DUPLICATION_LIMITS = {
|
|
|
149
152
|
minDirectiveChars: 40
|
|
150
153
|
};
|
|
151
154
|
const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
let YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
|
|
156
|
+
let YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
|
|
154
157
|
const REQUIRED_PACK_SECTIONS = [
|
|
155
158
|
'Product Direction',
|
|
156
159
|
'UI Direction',
|
|
@@ -986,8 +989,8 @@ async function studyNoteCheck(args = []) {
|
|
|
986
989
|
if (result.blockers.length) process.exitCode = 1;
|
|
987
990
|
}
|
|
988
991
|
|
|
989
|
-
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
|
|
990
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
992
|
+
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided', scope: any = null) {
|
|
993
|
+
const changedFileDetection = scope || gitChangedFileSnapshot(dir);
|
|
991
994
|
const changedFiles = changedFileDetection.files;
|
|
992
995
|
const changeScopeAvailable = changedFileDetection.available;
|
|
993
996
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
@@ -1022,6 +1025,13 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
1022
1025
|
for (const requirement of hygieneRequirements) {
|
|
1023
1026
|
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
1024
1027
|
}
|
|
1028
|
+
if (scope?.mode === 'advisory') {
|
|
1029
|
+
const ordinary = new Set(['changed_files', 'study_note_present', 'role_or_responsibility', 'verification', 'limits_or_uncertainty', 'next_step_present', 'single_study_note_next_step_pair', 'study_note_then_next_step']);
|
|
1030
|
+
for (const check of checks) if (!ordinary.has(check.id)) {
|
|
1031
|
+
check.status = 'skipped';
|
|
1032
|
+
check.truth_status = 'skipped';
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1025
1035
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
1026
1036
|
const result = {
|
|
1027
1037
|
schema: 'yam.study-note-guard.v1',
|
|
@@ -1108,7 +1118,7 @@ function hasNextStepMarker(text = '') {
|
|
|
1108
1118
|
|
|
1109
1119
|
function reportSectionMarkers(text = '') {
|
|
1110
1120
|
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1111
|
-
const trimmed = line.trim();
|
|
1121
|
+
const trimmed = line.trim().replace(/^\*\*(.*?)\*\*\s*:?[ \t]*$/, '$1');
|
|
1112
1122
|
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1113
1123
|
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1114
1124
|
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
@@ -1234,7 +1244,7 @@ function studyNoteHygieneRequirements(files = []) {
|
|
|
1234
1244
|
if (files.some((file) => /(^|\/)global\.css$/i.test(file))) {
|
|
1235
1245
|
requirements.push({ id: 'global_css_hygiene', terms: ['global.css', 'token', 'component', 'scoped', 'module', 'utility'], note: 'Study Note should say whether CSS belongs globally or should move to tokens/scoped/component styles' });
|
|
1236
1246
|
}
|
|
1237
|
-
if (files.some((file) => /(
|
|
1247
|
+
if (files.some((file) => /\.(sql|prisma)$/i.test(file) || /(^|\/)(migrations?|schema|database|drizzle|supabase)\//i.test(file) && /\.(ts|js)$/i.test(file))) {
|
|
1238
1248
|
requirements.push({ id: 'jsonb_hygiene', terms: ['jsonb', 'column', 'table', 'constraint', 'index', 'schema', 'relation'], note: 'Study Note should say whether structured product data belongs in typed columns/tables/constraints/indexes instead of broad DB jsonb' });
|
|
1239
1249
|
}
|
|
1240
1250
|
return requirements;
|
|
@@ -2014,6 +2024,7 @@ async function hook(args = []) {
|
|
|
2014
2024
|
const subcommand = args[0] || 'status';
|
|
2015
2025
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return hookUsage();
|
|
2016
2026
|
if (subcommand === 'status') return hookStatus(args.slice(1));
|
|
2027
|
+
if (subcommand === 'migrate') return hookMigrate(args.slice(1));
|
|
2017
2028
|
if (subcommand === 'enable') return hookEnable(args.slice(1));
|
|
2018
2029
|
if (subcommand === 'disable') return hookDisable(args.slice(1));
|
|
2019
2030
|
if (subcommand === 'run') return hookRun(args.slice(1));
|
|
@@ -2025,14 +2036,17 @@ function hookUsage() {
|
|
|
2025
2036
|
console.log(`yam hook
|
|
2026
2037
|
|
|
2027
2038
|
Usage:
|
|
2028
|
-
yam hook status [--global|--project dir]
|
|
2029
|
-
yam hook
|
|
2039
|
+
yam hook status [--global|--project dir] [--probe] [--json]
|
|
2040
|
+
yam hook migrate [--global|--project dir]
|
|
2041
|
+
yam hook enable <lite|study-note> [--global|--project dir] [--observe]
|
|
2030
2042
|
yam hook disable [lite|study-note] [--global|--project dir]
|
|
2031
2043
|
yam hook run <lite|study-note>
|
|
2032
2044
|
|
|
2033
2045
|
Notes:
|
|
2034
2046
|
hooks are opt-in. lite is advisory-only; study-note adds a prompt reminder and a one-pass Stop completion gate.
|
|
2035
2047
|
Hooks do not generate reports, run tmux, start subagents, or execute verification automatically.
|
|
2048
|
+
--probe queries hooks/list in a separate bounded Codex app-server; active session loading stays unknown.
|
|
2049
|
+
--observe opts into private local entrypoint observations without storing prompt content; not host attestation.
|
|
2036
2050
|
`);
|
|
2037
2051
|
}
|
|
2038
2052
|
|
|
@@ -2057,7 +2071,7 @@ function parseHookArgs(args = []) {
|
|
|
2057
2071
|
}
|
|
2058
2072
|
|
|
2059
2073
|
function hookPathFor(parsed) {
|
|
2060
|
-
if (parsed.mode === 'global') return path.join(os.homedir(), '.codex', 'hooks.json');
|
|
2074
|
+
if (parsed.mode === 'global') return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'hooks.json');
|
|
2061
2075
|
return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
|
|
2062
2076
|
}
|
|
2063
2077
|
|
|
@@ -2073,19 +2087,42 @@ function isYamHookProfile(handler: AnyRecord = {}, profile = 'lite') {
|
|
|
2073
2087
|
return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
|
|
2074
2088
|
}
|
|
2075
2089
|
|
|
2090
|
+
const CODEX_HOOK_EVENTS = new Set(['SessionStart', 'SessionEnd', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact', 'UserPromptSubmit', 'SubagentStart', 'SubagentStop', 'Stop', 'Interrupt']);
|
|
2091
|
+
|
|
2092
|
+
function hookEventMap(config: AnyRecord = {}) {
|
|
2093
|
+
const nested = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks) ? config.hooks : {};
|
|
2094
|
+
const events = { ...nested };
|
|
2095
|
+
for (const [event, entries] of Object.entries(config)) {
|
|
2096
|
+
if (CODEX_HOOK_EVENTS.has(event) && Array.isArray(entries)) {
|
|
2097
|
+
events[event] = [...(Array.isArray(events[event]) ? events[event] : []), ...entries];
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return events;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function canonicalHookConfig(config: AnyRecord = {}) {
|
|
2104
|
+
if (config.hooks != null && (typeof config.hooks !== 'object' || Array.isArray(config.hooks))) throw new Error('hooks must be an event object; existing file preserved');
|
|
2105
|
+
const next = { ...config, hooks: hookEventMap(config) };
|
|
2106
|
+
for (const event of CODEX_HOOK_EVENTS) {
|
|
2107
|
+
if (event in config && !Array.isArray(config[event])) throw new Error('legacy hook event must be an array; existing file preserved');
|
|
2108
|
+
delete next[event];
|
|
2109
|
+
}
|
|
2110
|
+
return next;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2076
2113
|
function stripYamHooks(config: AnyRecord = {}, profile = 'lite') {
|
|
2077
|
-
const next =
|
|
2078
|
-
for (const event of Object.keys(next)) {
|
|
2079
|
-
if (!Array.isArray(next[event])) continue;
|
|
2080
|
-
const entries = next[event];
|
|
2114
|
+
const next = canonicalHookConfig(config);
|
|
2115
|
+
for (const event of Object.keys(next.hooks)) {
|
|
2116
|
+
if (!Array.isArray(next.hooks[event])) continue;
|
|
2117
|
+
const entries = next.hooks[event];
|
|
2081
2118
|
const keptEntries = [];
|
|
2082
2119
|
for (const entry of entries) {
|
|
2083
2120
|
const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
|
|
2084
2121
|
const rest = { ...entry, hooks };
|
|
2085
2122
|
if (hooks.length > 0) keptEntries.push(rest);
|
|
2086
2123
|
}
|
|
2087
|
-
if (keptEntries.length > 0) next[event] = keptEntries;
|
|
2088
|
-
else delete next[event];
|
|
2124
|
+
if (keptEntries.length > 0) next.hooks[event] = keptEntries;
|
|
2125
|
+
else delete next.hooks[event];
|
|
2089
2126
|
}
|
|
2090
2127
|
return next;
|
|
2091
2128
|
}
|
|
@@ -2103,7 +2140,7 @@ function withYamHook(config: AnyRecord = {}, profile = 'lite') {
|
|
|
2103
2140
|
}
|
|
2104
2141
|
]
|
|
2105
2142
|
};
|
|
2106
|
-
next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
|
|
2143
|
+
next.hooks[event] = [...(Array.isArray(next.hooks[event]) ? next.hooks[event] : []), entry];
|
|
2107
2144
|
}
|
|
2108
2145
|
return next;
|
|
2109
2146
|
}
|
|
@@ -2113,6 +2150,11 @@ async function hookStatus(args = []) {
|
|
|
2113
2150
|
const target = hookPathFor(parsed);
|
|
2114
2151
|
const loaded = await readHookConfig(target);
|
|
2115
2152
|
if (loaded.error) {
|
|
2153
|
+
if (args.includes('--json')) {
|
|
2154
|
+
console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, configuration: 'unreadable', loaded: 'unknown', execution_observed: false, truth_status: 'partial' }, null, 2));
|
|
2155
|
+
process.exitCode = 1;
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2116
2158
|
console.log('yam-lite hook: broken');
|
|
2117
2159
|
console.log('yam-study-note hook: broken');
|
|
2118
2160
|
console.log(` - hook config unreadable: ${loaded.error}`);
|
|
@@ -2123,10 +2165,28 @@ async function hookStatus(args = []) {
|
|
|
2123
2165
|
}
|
|
2124
2166
|
const lite = await inspectHookProfile(loaded.config, 'lite');
|
|
2125
2167
|
const studyNote = await inspectHookProfile(loaded.config, 'study-note');
|
|
2168
|
+
const cwd = path.resolve(parsed.projectDir || process.cwd());
|
|
2169
|
+
const commands = { lite: lite.entries.map((entry) => String(entry.handler.command)), 'study-note': studyNote.entries.map((entry) => String(entry.handler.command)) };
|
|
2170
|
+
const host = args.includes('--probe')
|
|
2171
|
+
? await probeHookDiscovery({ executable: await findExecutable('codex') || 'codex', cwd, commands, configPath: target })
|
|
2172
|
+
: { state: 'not_measured', active_session_loaded: 'not_measured', execution_observed: false, truth_status: 'partial' };
|
|
2173
|
+
const profiles = {};
|
|
2174
|
+
for (const [profile, health] of [['lite', lite], ['study-note', studyNote]] as const) {
|
|
2175
|
+
profiles[profile] = { configured: health.state, issues: health.issues, loaded: 'unknown',
|
|
2176
|
+
execution: await inspectHookObservation(cwd, profile, YAM_HOOK_ENTRYPOINT) };
|
|
2177
|
+
}
|
|
2178
|
+
if (args.includes('--json')) {
|
|
2179
|
+
console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, profiles, host, truth_status: 'partial' }, null, 2));
|
|
2180
|
+
if (lite.state === 'broken' || studyNote.state === 'broken') process.exitCode = 1;
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2126
2183
|
printHookProfileStatus('lite', lite);
|
|
2127
2184
|
printHookProfileStatus('study-note', studyNote);
|
|
2128
2185
|
console.log(`scope: ${parsed.mode}`);
|
|
2129
2186
|
console.log(`file: ${target}`);
|
|
2187
|
+
console.log('active Codex session loaded: unknown (configuration is not runtime evidence)');
|
|
2188
|
+
console.log(`host discovery: ${host.state}`);
|
|
2189
|
+
for (const profile of ['lite', 'study-note']) console.log(`yam-${profile} execution: ${profiles[profile].execution.state} (local entrypoint evidence, not host attestation)`);
|
|
2130
2190
|
if (lite.state === 'broken' || studyNote.state === 'broken') process.exitCode = 1;
|
|
2131
2191
|
}
|
|
2132
2192
|
|
|
@@ -2135,7 +2195,7 @@ function hookConfigHasYamLite(config = {}) {
|
|
|
2135
2195
|
}
|
|
2136
2196
|
|
|
2137
2197
|
function hookConfigHasProfile(config = {}, profile = 'lite') {
|
|
2138
|
-
return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
2198
|
+
return Object.values(hookEventMap(config)).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
2139
2199
|
return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
|
|
2140
2200
|
}));
|
|
2141
2201
|
}
|
|
@@ -2150,7 +2210,7 @@ function hookEventsForProfile(profile = 'lite') {
|
|
|
2150
2210
|
|
|
2151
2211
|
function hookProfileEntries(config: AnyRecord = {}, profile = 'lite') {
|
|
2152
2212
|
const matches = [];
|
|
2153
|
-
for (const [event, entries] of Object.entries(config)) {
|
|
2213
|
+
for (const [event, entries] of Object.entries(hookEventMap(config))) {
|
|
2154
2214
|
if (!Array.isArray(entries)) continue;
|
|
2155
2215
|
for (const entry of entries) {
|
|
2156
2216
|
if (!Array.isArray(entry?.hooks)) continue;
|
|
@@ -2209,6 +2269,7 @@ function splitHookCommand(command = '') {
|
|
|
2209
2269
|
|
|
2210
2270
|
function parseYamHookCommand(command = '', profile = 'lite') {
|
|
2211
2271
|
const words = splitHookCommand(command);
|
|
2272
|
+
if (words.at(-1) === '--observe') words.pop();
|
|
2212
2273
|
if (words.length === 5 && words[2] === 'hook' && words[3] === 'run' && words[4] === profile) {
|
|
2213
2274
|
const nodeName = path.basename(words[0]).toLowerCase();
|
|
2214
2275
|
if (/^node(?:\.exe)?$/.test(nodeName) && path.basename(words[1]).toLowerCase() === 'yam.js') {
|
|
@@ -2249,6 +2310,7 @@ async function inspectHookProfile(config: AnyRecord = {}, profile = 'lite') {
|
|
|
2249
2310
|
const entries = hookProfileEntries(config, profile);
|
|
2250
2311
|
if (!entries.length) return { state: 'disabled', issues: [], entries };
|
|
2251
2312
|
const issues = [];
|
|
2313
|
+
if (Object.keys(config).some((event) => CODEX_HOOK_EVENTS.has(event))) issues.push('legacy hook config requires the top-level hooks wrapper; rerun yam hook enable to migrate with backup');
|
|
2252
2314
|
for (const event of hookEventsForProfile(profile)) {
|
|
2253
2315
|
const count = entries.filter((entry) => entry.event === event).length;
|
|
2254
2316
|
if (count === 0) issues.push(`${event} handler is missing; rerun yam hook enable ${profile}`);
|
|
@@ -2275,6 +2337,9 @@ async function readHookConfig(target) {
|
|
|
2275
2337
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
2276
2338
|
return { config: {}, exists: true, error: 'top-level value must be a JSON object' };
|
|
2277
2339
|
}
|
|
2340
|
+
if (value.hooks != null && (typeof value.hooks !== 'object' || Array.isArray(value.hooks))) {
|
|
2341
|
+
return { config: {}, exists: true, error: 'hooks must be an event object' };
|
|
2342
|
+
}
|
|
2278
2343
|
return { config: value, exists: true, error: '' };
|
|
2279
2344
|
} catch (error) {
|
|
2280
2345
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { config: {}, exists: false, error: '' };
|
|
@@ -2294,8 +2359,34 @@ async function writeHookConfig(target, config) {
|
|
|
2294
2359
|
}
|
|
2295
2360
|
}
|
|
2296
2361
|
|
|
2362
|
+
async function hookMigrate(args = []) {
|
|
2363
|
+
const parsed = parseHookArgs(args);
|
|
2364
|
+
const target = hookPathFor(parsed);
|
|
2365
|
+
const loaded = await readHookConfig(target);
|
|
2366
|
+
if (loaded.error) { console.error('hook migration blocked: unreadable configuration'); process.exitCode = 1; return; }
|
|
2367
|
+
if (!loaded.exists || !Object.keys(loaded.config).some((key) => CODEX_HOOK_EVENTS.has(key))) {
|
|
2368
|
+
console.log('hook migration: no legacy event layout found'); return;
|
|
2369
|
+
}
|
|
2370
|
+
const before = await fsp.lstat(target);
|
|
2371
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) throw new Error('hook migration blocked: unsafe config file');
|
|
2372
|
+
const original = await fsp.readFile(target, 'utf8');
|
|
2373
|
+
if (JSON.stringify(JSON.parse(original)) !== JSON.stringify(loaded.config)) throw new Error('hook configuration changed; retry migration');
|
|
2374
|
+
const next = canonicalHookConfig(loaded.config);
|
|
2375
|
+
const backup = `${target}.yam-backup-${timestampId()}`;
|
|
2376
|
+
await fsp.copyFile(target, backup, fs.constants.COPYFILE_EXCL);
|
|
2377
|
+
const after = await fsp.lstat(target);
|
|
2378
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.mtimeMs !== before.mtimeMs
|
|
2379
|
+
|| await fsp.readFile(target, 'utf8') !== original) throw new Error('hook configuration changed; backup preserved, retry migration');
|
|
2380
|
+
await writeHookConfig(target, next);
|
|
2381
|
+
console.log(`hook migration: wrapped event groups; handlers and metadata preserved\nbackup: ${backup}\nReview changed definitions in Codex /hooks before enabling execution.`);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2297
2384
|
async function hookEnable(args = []) {
|
|
2298
2385
|
const parsed = parseHookArgs(args);
|
|
2386
|
+
const node = await selectHookNode();
|
|
2387
|
+
const observe = args.includes('--observe') ? ' --observe' : '';
|
|
2388
|
+
YAM_LITE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite${observe}`;
|
|
2389
|
+
YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note${observe}`;
|
|
2299
2390
|
if (!['lite', 'study-note'].includes(parsed.profile)) {
|
|
2300
2391
|
console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
|
|
2301
2392
|
process.exitCode = 1;
|
|
@@ -2326,6 +2417,7 @@ async function hookEnable(args = []) {
|
|
|
2326
2417
|
}
|
|
2327
2418
|
await writeHookConfig(target, next);
|
|
2328
2419
|
console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
|
|
2420
|
+
console.log(`node launcher: ${node.selection}; alias identity matched the current runtime (no PATH candidate executed)`);
|
|
2329
2421
|
if (needsMigration) console.log(`migrated: ${existing.length} existing yam-${parsed.profile} handler(s) to the current command and event coverage`);
|
|
2330
2422
|
console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
|
|
2331
2423
|
}
|
|
@@ -2346,7 +2438,7 @@ async function hookDisable(args = []) {
|
|
|
2346
2438
|
return;
|
|
2347
2439
|
}
|
|
2348
2440
|
const next = stripYamHooks(current, parsed.profile);
|
|
2349
|
-
if (Object.keys(next).length === 0) {
|
|
2441
|
+
if (Object.keys(next).length === 1 && Object.keys(next.hooks).length === 0) {
|
|
2350
2442
|
await rmrf(target);
|
|
2351
2443
|
} else {
|
|
2352
2444
|
await writeHookConfig(target, next);
|
|
@@ -2374,13 +2466,14 @@ async function hookRun(args = []) {
|
|
|
2374
2466
|
}
|
|
2375
2467
|
const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
|
|
2376
2468
|
const cwd = String(input?.cwd || process.cwd());
|
|
2469
|
+
if (args.includes('--observe')) await recordHookObservation(cwd, profile, String(event), YAM_HOOK_ENTRYPOINT).catch(() => {});
|
|
2377
2470
|
if (profile === 'study-note' && event === 'Stop') {
|
|
2378
2471
|
console.log(JSON.stringify(await buildStudyNoteStopOutput(input, cwd)));
|
|
2379
2472
|
return;
|
|
2380
2473
|
}
|
|
2381
2474
|
const prompt = extractPrompt(input);
|
|
2382
2475
|
const additionalContext = profile === 'study-note'
|
|
2383
|
-
? await
|
|
2476
|
+
? await buildStudyNoteTurnContext(input, cwd)
|
|
2384
2477
|
: await buildYamLiteContext({ cwd, prompt });
|
|
2385
2478
|
const output = {
|
|
2386
2479
|
continue: true,
|
|
@@ -2412,6 +2505,7 @@ function extractPrompt(input: AnyRecord = {}) {
|
|
|
2412
2505
|
async function buildYamLiteContext({ cwd, prompt }) {
|
|
2413
2506
|
const lines = [
|
|
2414
2507
|
'yam-lite guide active: keep project direction visible, preserve momentum, and deepen when scope, risk, or user intent calls for it.',
|
|
2508
|
+
'Git workflow: keep the current branch; create/switch branches, worktrees or PRs only on explicit user request or applicable project rule. Commit/push, release bumps, branch-name prefixes and protection alone do not authorize them. If blocked, report and ask; do not silently change workflow or protections.',
|
|
2415
2509
|
'Default: do a basic direction-fit and honest-verification check; avoid broad reading for small work and never claim proof without evidence.'
|
|
2416
2510
|
];
|
|
2417
2511
|
const pack = await findProjectPack(cwd).catch(() => null);
|
|
@@ -2427,38 +2521,10 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
2427
2521
|
return lines.join('\n');
|
|
2428
2522
|
}
|
|
2429
2523
|
|
|
2430
|
-
async function buildStudyNoteHookContext({ cwd }) {
|
|
2431
|
-
const dir = path.resolve(cwd || process.cwd());
|
|
2432
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2433
|
-
const changedFiles = changedFileDetection.files;
|
|
2434
|
-
const lines = [
|
|
2435
|
-
'yam Study Note guard active: if this turn changes code, config, release metadata, docs, or project artifacts, include a Study Note followed immediately by Next step in the final response.'
|
|
2436
|
-
];
|
|
2437
|
-
if (!changedFileDetection.available) {
|
|
2438
|
-
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2439
|
-
lines.push('Determine manually whether artifacts changed and include Study Note followed by Next step when they did; the Stop hook will keep this uncertainty visible.');
|
|
2440
|
-
return lines.join('\n');
|
|
2441
|
-
}
|
|
2442
|
-
if (!changedFiles.length) {
|
|
2443
|
-
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add Study Note followed immediately by Next step before final.');
|
|
2444
|
-
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2445
|
-
return lines.join('\n');
|
|
2446
|
-
}
|
|
2447
|
-
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2448
|
-
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2449
|
-
lines.push('Next step minimum: quick whole-process scan, current situation, forward outlook, critical opinion, improvement recommendation, ordered fix-first then planned actions, evidence, ownership, blocker status, safe retry, side effects, and truth status.');
|
|
2450
|
-
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2451
|
-
if (hygiene.length) {
|
|
2452
|
-
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2453
|
-
lines.push('Report whether the change avoided dumping unrelated logic into page.tsx, one-off CSS into global.css, or structured product data into broad DB jsonb.');
|
|
2454
|
-
}
|
|
2455
|
-
lines.push('This prompt reminder does not generate or edit either section; the paired Stop hook requests one correction pass if the final response is incomplete or ordered incorrectly.');
|
|
2456
|
-
return lines.join('\n');
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
2524
|
async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd()) {
|
|
2460
2525
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
2461
|
-
const
|
|
2526
|
+
const scope = studyNoteTurnScope(input, cwd);
|
|
2527
|
+
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message', scope);
|
|
2462
2528
|
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2463
2529
|
return {
|
|
2464
2530
|
continue: true,
|
|
@@ -2466,7 +2532,11 @@ async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd
|
|
|
2466
2532
|
};
|
|
2467
2533
|
}
|
|
2468
2534
|
if (!result.blockers.length) return { continue: true };
|
|
2469
|
-
const
|
|
2535
|
+
const failed = result.checks.filter(check => check.status === 'fail').map(check => check.id);
|
|
2536
|
+
const summary = failed.includes('study_note_present') ? 'study_note_present' : failed.join(', ');
|
|
2537
|
+
if (scope.mode !== 'strict') {
|
|
2538
|
+
return { continue: true, systemMessage: `yam Study Note advisory — missing fields: ${summary}. Keep ordinary work concise; no correction gate applied.` };
|
|
2539
|
+
}
|
|
2470
2540
|
if (Boolean(input?.stop_hook_active || input?.stopHookActive)) {
|
|
2471
2541
|
return {
|
|
2472
2542
|
continue: true,
|
|
@@ -2476,13 +2546,18 @@ async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd
|
|
|
2476
2546
|
return {
|
|
2477
2547
|
decision: 'block',
|
|
2478
2548
|
reason: [
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
'Revise the final response to include: touched artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, limits, and any relevant architecture hygiene.'
|
|
2549
|
+
`yam Study Note strict — missing fields: ${summary}.`,
|
|
2550
|
+
'Revise only the missing fields in Study Note / Next step; this is report validation, not a security scan.'
|
|
2482
2551
|
].join(' ')
|
|
2483
2552
|
};
|
|
2484
2553
|
}
|
|
2485
2554
|
|
|
2555
|
+
async function buildStudyNoteTurnContext(input: AnyRecord, cwd: string) {
|
|
2556
|
+
const scope = studyNoteTurnScope(input, cwd, true);
|
|
2557
|
+
if (!scope.available) return 'yam Study Note: turn scope is unavailable; do not treat this as a clean project. Inspect changed artifacts manually; no strict block will be applied without a baseline.';
|
|
2558
|
+
return `yam Study Note turn baseline saved (hash-only). Mode: ${scope.mode}. Only changes since this prompt count; known generated logs, mission/security receipts and screenshots are excluded unless explicitly included. Include a concise Study Note followed by Next step when product artifacts change. ${scope.mode === 'strict' ? 'Risk work uses the detailed report contract and one correction pass.' : 'Ordinary work is advisory: a short explanation, verification/limits and remaining work (or none) suffice.'}`;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2486
2561
|
function yamLiteRouteHint(prompt = '') {
|
|
2487
2562
|
const text = String(prompt || '').toLowerCase();
|
|
2488
2563
|
if (!text) return '';
|
|
@@ -6109,7 +6184,7 @@ async function memory(args = []) {
|
|
|
6109
6184
|
}
|
|
6110
6185
|
if (subcommand === 'summary' || subcommand === 'summarize') {
|
|
6111
6186
|
const parsed = parseMemoryArgs(args.slice(1));
|
|
6112
|
-
return memorySummary(parsed.dir);
|
|
6187
|
+
return memorySummary(parsed.dir, { json: args.includes('--json') });
|
|
6113
6188
|
}
|
|
6114
6189
|
if (subcommand === 'resolve') return memoryResolve(args.slice(1));
|
|
6115
6190
|
console.error(`unknown memory command: ${subcommand}`);
|
|
@@ -6123,7 +6198,7 @@ Usage:
|
|
|
6123
6198
|
yam memory init [dir]
|
|
6124
6199
|
yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
|
|
6125
6200
|
yam memory list [dir] [--json]
|
|
6126
|
-
yam memory summary [dir]
|
|
6201
|
+
yam memory summary [dir] [--json]
|
|
6127
6202
|
yam memory resolve [dir] <id> [--note <text>]
|
|
6128
6203
|
|
|
6129
6204
|
Kinds:
|
|
@@ -6198,7 +6273,14 @@ async function memoryAdd(args = []) {
|
|
|
6198
6273
|
}
|
|
6199
6274
|
|
|
6200
6275
|
async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
6201
|
-
const
|
|
6276
|
+
const report = await readMemoryRecords(targetDir);
|
|
6277
|
+
const records = report.records;
|
|
6278
|
+
if (!report.complete) {
|
|
6279
|
+
if (report.state !== 'missing') process.exitCode = 1;
|
|
6280
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
6281
|
+
else console.error(`memory read ${report.state}: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
|
|
6282
|
+
return;
|
|
6283
|
+
}
|
|
6202
6284
|
if (json) {
|
|
6203
6285
|
console.log(JSON.stringify(records, null, 2));
|
|
6204
6286
|
return;
|
|
@@ -6213,9 +6295,16 @@ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
|
6213
6295
|
}
|
|
6214
6296
|
}
|
|
6215
6297
|
|
|
6216
|
-
async function memorySummary(targetDir = process.cwd()) {
|
|
6298
|
+
async function memorySummary(targetDir = process.cwd(), { json = false } = {}) {
|
|
6217
6299
|
const dir = memoryDir(targetDir);
|
|
6218
|
-
const
|
|
6300
|
+
const report = await readMemoryRecords(targetDir);
|
|
6301
|
+
if (!report.complete) {
|
|
6302
|
+
process.exitCode = 1;
|
|
6303
|
+
if (json) console.log(JSON.stringify({ ...report, summary_written: false }, null, 2));
|
|
6304
|
+
else console.error(`memory summary blocked (${report.state}); existing summary preserved: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6307
|
+
const records = report.records;
|
|
6219
6308
|
await fsp.mkdir(dir, { recursive: true });
|
|
6220
6309
|
const active = records.filter((record) => record.status !== 'resolved');
|
|
6221
6310
|
const resolved = records.filter((record) => record.status === 'resolved');
|
|
@@ -6246,7 +6335,8 @@ async function memorySummary(targetDir = process.cwd()) {
|
|
|
6246
6335
|
|
|
6247
6336
|
const target = path.join(dir, 'summary.md');
|
|
6248
6337
|
await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
|
|
6249
|
-
console.log(
|
|
6338
|
+
if (json) console.log(JSON.stringify({ ...report, summary_written: true, path: target }, null, 2));
|
|
6339
|
+
else console.log(`memory summary written: ${target}`);
|
|
6250
6340
|
}
|
|
6251
6341
|
|
|
6252
6342
|
async function memoryResolve(args = []) {
|
|
@@ -6305,20 +6395,7 @@ function memoryDir(targetDir = process.cwd()) {
|
|
|
6305
6395
|
|
|
6306
6396
|
async function readMemoryRecords(targetDir = process.cwd()) {
|
|
6307
6397
|
const recordsDir = path.join(memoryDir(targetDir), 'records');
|
|
6308
|
-
|
|
6309
|
-
const entries = await fsp.readdir(recordsDir, { withFileTypes: true });
|
|
6310
|
-
const records = [];
|
|
6311
|
-
for (const entry of entries) {
|
|
6312
|
-
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
6313
|
-
const file = path.join(recordsDir, entry.name);
|
|
6314
|
-
try {
|
|
6315
|
-
const record = await readJson(file);
|
|
6316
|
-
records.push(record);
|
|
6317
|
-
} catch (error) {
|
|
6318
|
-
records.push({ id: entry.name.replace(/\.json$/, ''), kind: 'invalid', status: 'invalid', summary: `invalid JSON: ${errorMessage(error)}` });
|
|
6319
|
-
}
|
|
6320
|
-
}
|
|
6321
|
-
return records.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
6398
|
+
return readMemoryDirectory(recordsDir);
|
|
6322
6399
|
}
|
|
6323
6400
|
|
|
6324
6401
|
function timestampId() {
|
|
@@ -34,6 +34,7 @@ export interface ExternalUpdatePaths {
|
|
|
34
34
|
export interface ExternalUpdateDependencies {
|
|
35
35
|
run?: (command: string, args: string[], options?: ExternalCommandOptions) => Promise<ExternalCommandResult> | ExternalCommandResult;
|
|
36
36
|
fetchJson?: (url: string) => Promise<any>;
|
|
37
|
+
probeArtifact?: (url: string) => Promise<number>;
|
|
37
38
|
now?: () => Date;
|
|
38
39
|
homeDir?: string;
|
|
39
40
|
env?: NodeJS.ProcessEnv;
|
|
@@ -46,7 +47,7 @@ export interface ExternalComponentCheck {
|
|
|
46
47
|
installed_version: string;
|
|
47
48
|
latest_version: string;
|
|
48
49
|
update_available: boolean;
|
|
49
|
-
status: 'up_to_date' | 'update_available' | 'not_installed' | 'check_failed';
|
|
50
|
+
status: 'up_to_date' | 'update_available' | 'not_installed' | 'pending_distribution' | 'check_failed';
|
|
50
51
|
source: string;
|
|
51
52
|
source_revision?: {
|
|
52
53
|
local: string;
|
|
@@ -113,6 +114,7 @@ export interface YamInstallIdentity {
|
|
|
113
114
|
interface ResolvedDependencies {
|
|
114
115
|
run: NonNullable<ExternalUpdateDependencies['run']>;
|
|
115
116
|
fetchJson: NonNullable<ExternalUpdateDependencies['fetchJson']>;
|
|
117
|
+
probeArtifact: NonNullable<ExternalUpdateDependencies['probeArtifact']>;
|
|
116
118
|
now: NonNullable<ExternalUpdateDependencies['now']>;
|
|
117
119
|
homeDir: string;
|
|
118
120
|
env: NodeJS.ProcessEnv;
|
|
@@ -158,7 +160,7 @@ export async function checkExternalUpdates(
|
|
|
158
160
|
checkScrapling(deps),
|
|
159
161
|
checkInsaneSearch(deps)
|
|
160
162
|
]);
|
|
161
|
-
const failed = checks.filter((item) => item.status === 'check_failed');
|
|
163
|
+
const failed = checks.filter((item) => item.status === 'check_failed' || item.status === 'pending_distribution');
|
|
162
164
|
return {
|
|
163
165
|
schema: 'yam.external-update-check.v1',
|
|
164
166
|
generated_at: deps.now().toISOString(),
|
|
@@ -351,6 +353,7 @@ async function resolveDependencies(input: ExternalUpdateDependencies): Promise<R
|
|
|
351
353
|
return {
|
|
352
354
|
run,
|
|
353
355
|
fetchJson: input.fetchJson || defaultFetchJson,
|
|
356
|
+
probeArtifact: input.probeArtifact || defaultProbeArtifact,
|
|
354
357
|
now: input.now || (() => new Date()),
|
|
355
358
|
homeDir,
|
|
356
359
|
env,
|
|
@@ -409,18 +412,52 @@ async function acquireApplyLock(deps: ResolvedDependencies) {
|
|
|
409
412
|
}
|
|
410
413
|
|
|
411
414
|
async function checkYam(currentYamVersion: string, deps: ResolvedDependencies): Promise<ExternalComponentCheck> {
|
|
415
|
+
let pending: ExternalComponentCheck | undefined;
|
|
412
416
|
try {
|
|
413
417
|
const data = await deps.fetchJson(YAM_REGISTRY_URL);
|
|
414
418
|
const latest = requireStableVersion(data?.version, 'yam registry version');
|
|
415
419
|
const installed = requireStableVersion(currentYamVersion, 'installed yam version');
|
|
416
420
|
const integrity = requireIntegrityValue(data?.dist?.integrity, 'yam registry integrity');
|
|
417
421
|
const revision = requireGitRevision(data?.gitHead, 'yam registry gitHead');
|
|
422
|
+
if (data?.name !== 'yam-flow') throw new Error('yam registry package name mismatch');
|
|
423
|
+
const tarball = requireYamTarball(data?.dist?.tarball, latest);
|
|
424
|
+
const exactUrl = `${NPM_REGISTRY}yam-flow/${latest}`;
|
|
425
|
+
pending = {
|
|
426
|
+
...componentCheck('yam', installed, latest, 'npm:yam-flow'),
|
|
427
|
+
status: 'pending_distribution',
|
|
428
|
+
error: 'pending_distribution: exact-version metadata or tarball returned HTTP 404; retry the explicit update later',
|
|
429
|
+
source_receipt: {
|
|
430
|
+
canonical_url: YAM_REGISTRY_URL,
|
|
431
|
+
retrieved_at: deps.now().toISOString(),
|
|
432
|
+
access_path: 'npm latest observed; exact-version distribution readiness incomplete',
|
|
433
|
+
version: latest,
|
|
434
|
+
revision,
|
|
435
|
+
integrity: [integrity]
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
let exact: any;
|
|
439
|
+
try {
|
|
440
|
+
exact = await deps.fetchJson(exactUrl);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof RegistryHttpError && error.status === 404) return pending;
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
if (exact?.name !== 'yam-flow' || data?.name !== 'yam-flow'
|
|
446
|
+
|| requireStableVersion(exact?.version, 'yam exact registry version') !== latest
|
|
447
|
+
|| requireIntegrityValue(exact?.dist?.integrity, 'yam exact registry integrity') !== integrity
|
|
448
|
+
|| requireGitRevision(exact?.gitHead, 'yam exact registry gitHead') !== revision
|
|
449
|
+
|| requireYamTarball(exact?.dist?.tarball, latest) !== tarball) {
|
|
450
|
+
throw new Error('yam latest and exact-version registry metadata disagree');
|
|
451
|
+
}
|
|
452
|
+
const artifactStatus = await deps.probeArtifact(tarball);
|
|
453
|
+
if (artifactStatus === 404) return pending;
|
|
454
|
+
if (artifactStatus !== 200) throw new Error(`yam tarball availability check failed: HTTP ${artifactStatus}`);
|
|
418
455
|
return {
|
|
419
456
|
...componentCheck('yam', installed, latest, 'npm:yam-flow'),
|
|
420
457
|
source_receipt: {
|
|
421
458
|
canonical_url: YAM_REGISTRY_URL,
|
|
422
459
|
retrieved_at: deps.now().toISOString(),
|
|
423
|
-
access_path: 'npm
|
|
460
|
+
access_path: 'npm latest + exact-version metadata + bounded no-redirect tarball HEAD (availability only)',
|
|
424
461
|
version: latest,
|
|
425
462
|
revision,
|
|
426
463
|
integrity: [integrity]
|
|
@@ -601,7 +638,7 @@ function failedCheck(
|
|
|
601
638
|
|
|
602
639
|
async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
|
|
603
640
|
const check = await checkYam(currentYamVersion, deps);
|
|
604
|
-
if (check.status === 'check_failed') return persistFailureFromCheck(check, deps);
|
|
641
|
+
if (check.status === 'check_failed' || check.status === 'pending_distribution') return persistFailureFromCheck(check, deps);
|
|
605
642
|
if (!check.update_available) {
|
|
606
643
|
return persistReceipt(baseReceipt(check, 'up_to_date', [], {
|
|
607
644
|
automatic: true,
|
|
@@ -1609,14 +1646,58 @@ async function defaultRun(command: string, args: string[], options: ExternalComm
|
|
|
1609
1646
|
|
|
1610
1647
|
async function defaultFetchJson(url: string) {
|
|
1611
1648
|
const response = await fetch(url, {
|
|
1649
|
+
redirect: 'error',
|
|
1650
|
+
credentials: 'omit',
|
|
1612
1651
|
headers: {
|
|
1613
1652
|
accept: 'application/json',
|
|
1614
1653
|
'user-agent': 'yam-flow external-update-check'
|
|
1615
1654
|
},
|
|
1616
1655
|
signal: AbortSignal.timeout(30000)
|
|
1617
1656
|
});
|
|
1618
|
-
if (!response.ok)
|
|
1619
|
-
|
|
1657
|
+
if (!response.ok) {
|
|
1658
|
+
await response.body?.cancel();
|
|
1659
|
+
throw new RegistryHttpError(response.status, url);
|
|
1660
|
+
}
|
|
1661
|
+
const reader = response.body?.getReader();
|
|
1662
|
+
if (!reader) throw new Error(`empty registry response for ${url}`);
|
|
1663
|
+
const chunks: Uint8Array[] = [];
|
|
1664
|
+
let size = 0;
|
|
1665
|
+
try {
|
|
1666
|
+
while (true) {
|
|
1667
|
+
const chunk = await reader.read();
|
|
1668
|
+
if (chunk.done) break;
|
|
1669
|
+
size += chunk.value.byteLength;
|
|
1670
|
+
if (size > 1024 * 1024) throw new Error('registry metadata exceeds 1 MiB limit');
|
|
1671
|
+
chunks.push(chunk.value);
|
|
1672
|
+
}
|
|
1673
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1674
|
+
} finally {
|
|
1675
|
+
await reader.cancel().catch(() => undefined);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
export class RegistryHttpError extends Error {
|
|
1680
|
+
constructor(readonly status: number, url: string) {
|
|
1681
|
+
super(`HTTP ${status} for ${url}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function requireYamTarball(value: unknown, version: string) {
|
|
1686
|
+
const expected = `${NPM_REGISTRY}yam-flow/-/yam-flow-${version}.tgz`;
|
|
1687
|
+
if (value !== expected) throw new Error('yam tarball must be the exact-version artifact on the official npm registry');
|
|
1688
|
+
return expected;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
async function defaultProbeArtifact(url: string) {
|
|
1692
|
+
const response = await fetch(url, {
|
|
1693
|
+
method: 'HEAD',
|
|
1694
|
+
redirect: 'error',
|
|
1695
|
+
credentials: 'omit',
|
|
1696
|
+
headers: { 'user-agent': 'yam-flow external-update-check' },
|
|
1697
|
+
signal: AbortSignal.timeout(10000)
|
|
1698
|
+
});
|
|
1699
|
+
await response.body?.cancel();
|
|
1700
|
+
return response.status;
|
|
1620
1701
|
}
|
|
1621
1702
|
|
|
1622
1703
|
function requireStableVersion(value: unknown, label: string) {
|