yam-flow 2.8.1 → 2.8.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to yam-flow are documented here.
4
4
 
5
+ ## v2.8.2 - 2026-10-02
6
+
7
+ ### Improved
8
+
9
+ - Hook configuration now uses Codex's required top-level `hooks` wrapper. `yam hook migrate` backs up legacy files and preserves commands and unrelated handlers without automatically approving Codex trust.
10
+ - Hook status can now separate configured health, Codex `hooks/list` discovery, and opt-in local entrypoint observations; Node launcher selection is bound to the running executable identity.
11
+ - External yam update checks distinguish exact-version npm distribution propagation (`pending_distribution`) from network, authorization, or metadata failures.
12
+ - Memory listing and summary preserve the difference between missing, empty, invalid, unreadable, and incomplete record storage; incomplete reads cannot overwrite an existing summary.
13
+ - Added bounded Markdown skill-content checks for frontmatter identity, symlinks, personal absolute paths, download-to-shell patterns, and high-signal secrets.
14
+
5
15
  ## v2.8.1 - 2026-10-01
6
16
 
7
17
  ### Security
package/COMMANDS.md CHANGED
@@ -153,6 +153,8 @@ yam memory add /path/to/project --kind repeat_mistake --summary "UI work was dec
153
153
  yam memory summary /path/to/project
154
154
  ```
155
155
 
156
+ `yam memory list --json` keeps the normal complete result as an array and returns a bounded `yam.memory-read.v1` envelope for missing, invalid, unreadable, or incomplete storage. `yam memory summary --json` refuses to overwrite the existing summary unless the record directory was read completely.
157
+
156
158
  ## Token Budget
157
159
 
158
160
  ```bash
@@ -172,7 +174,7 @@ yam update apply --component yam --json
172
174
  yam update apply --all --json
173
175
  ```
174
176
 
175
- `yam update check` is read-only. Its component result preserves the registry artifact source receipt: npm integrity/`gitHead` for yam, PyPI release-file SHA-256 values for Scrapling, and the Git-pinned manifest commit for Insane Search. Every `apply` needs explicit authorization, pins the official npm registry or isolated PyPI index, uses a concurrent-run lock, records a component receipt, and stops on failed/manual results. `--all` runs yam last. A yam install verifies the effective executable identity before any yam-side install command and repeats that check during rollback. This identity contract currently supports POSIX symlink layouts; Windows npm `.cmd` shims fail closed as unverified. Insane Search requires a clean tracked local manifest whose bytes match Git and matching pre/post-add commits, is never remove-first, and never edits `.codex/plugins/cache` directly.
177
+ `yam update check` is read-only. Its component result preserves the registry artifact source receipt: npm integrity/`gitHead` for yam, PyPI release-file SHA-256 values for Scrapling, and the Git-pinned manifest commit for Insane Search. Yam also compares `latest` with exact-version metadata and probes the official exact tarball URL; a 404 is `pending_distribution`, while network/auth/metadata mismatch is `check_failed`. The bounded HEAD probe confirms availability only and does not hash tarball bytes. Every `apply` needs explicit authorization, pins the official npm registry or isolated PyPI index, uses a concurrent-run lock, records a component receipt, and stops on failed/manual results. `--all` runs yam last. A yam install verifies the effective executable identity before any yam-side install command and repeats that check during rollback. This identity contract currently supports POSIX symlink layouts; Windows npm `.cmd` shims fail closed as unverified. Insane Search requires a clean tracked local manifest whose bytes match Git and matching pre/post-add commits, is never remove-first, and never edits `.codex/plugins/cache` directly.
176
178
 
177
179
  ## Release Supply Chain
178
180
 
@@ -327,15 +329,18 @@ Supported report shapes:
327
329
 
328
330
  `lite` is advisory-only. `study-note` runs the read-only Study Note Guard against the final assistant message and can request one correction pass; neither profile runs verification or forces routes.
329
331
 
332
+ `yam hook migrate [--global|--project dir]` wraps legacy top-level events in Codex's `hooks` object without changing commands. It backs up the original, refuses unsafe files, and is a no-op for canonical configs. Review changed definitions in Codex `/hooks` before execution; yam does not approve trust.
333
+
330
334
  ```bash
331
335
  yam hook status --global
336
+ yam hook migrate --global
332
337
  yam hook enable lite --global
333
338
  yam hook enable study-note --global
334
339
  yam hook disable lite --global
335
340
  yam hook disable study-note --global
336
341
  ```
337
342
 
338
- `yam hook status` exits nonzero for unreadable configs, stale paths, missing targets, duplicate handlers, or incomplete event coverage. Re-enable the affected profile to back up the file, preserve unrelated hooks, and migrate its command and events.
343
+ `yam hook status` exits nonzero for unreadable configs, stale paths, missing targets, duplicate handlers, or incomplete event coverage. Add `--json` for `yam.hook-readiness.v1`, or `--probe` to query `hooks/list` in a separate bounded Codex app-server. The probe separates host discovery from local configuration and never claims that the active Desktop session loaded the hook. Add `--observe` to `yam hook enable` to record a bounded local entrypoint observation; it contains only hashes and event metadata and remains partial same-user evidence. Re-enable the affected profile to back up the file, preserve unrelated hooks, and migrate its command and events.
339
344
 
340
345
  ## Security check
341
346
 
package/README.md CHANGED
@@ -74,6 +74,8 @@ yam memory add . --kind lesson --summary "Keep UI checks visual before declaring
74
74
  yam memory summary .
75
75
  ```
76
76
 
77
+ `yam memory list --json` distinguishes missing, empty, invalid, unreadable, and incomplete record directories. A blocked `yam memory summary --json` exits nonzero and preserves the previous summary instead of treating an access failure as an empty memory set.
78
+
77
79
  Memory writes to `.yam/memory/` only when you run the command. Routes should prefer `.yam/memory/summary.md` and should not read every record by default.
78
80
 
79
81
  ## Lite Hook
@@ -91,6 +93,8 @@ It only adds short advisory context. It does not run verification, tmux, browser
91
93
 
92
94
  ## Study Note Hook
93
95
 
96
+ Codex hook files use a top-level `hooks` object containing event groups (`{"hooks":{"Stop":[...]}}`). For older yam files with events at the top level, run `yam hook migrate --global`: this creates a backup and changes only the layout, preserving existing commands and unrelated handlers. Review the changed definitions in Codex `/hooks`; discovery alone does not authorize execution, and yam never approves hook trust automatically.
97
+
94
98
  Enable this profile when you want Codex to check changed work at final-response time as well as remind the agent at prompt time.
95
99
 
96
100
  ```bash
@@ -102,7 +106,7 @@ The profile installs `UserPromptSubmit` and `Stop` handlers. At `Stop`, yam chec
102
106
 
103
107
  Hook stdin is limited to 1 MiB. Malformed JSON or an oversized payload produces a valid fail-open response without echoing the rejected content, deriving hook context from it, or mutating the workspace. Other CLI commands that read piped text use a 4 MiB limit and fail closed when it is exceeded.
104
108
 
105
- `yam hook status` reports stale or missing command targets as `broken` and exits nonzero. Run `yam hook enable <profile> --global` again to create a timestamped backup, preserve unrelated hooks, and migrate that profile to the current installed path and event coverage. Restart Codex or open a new task after changing hooks.
109
+ `yam hook status` reports stale or missing command targets as `broken` and exits nonzero. Run `yam hook enable <profile> --global` again to create a timestamped backup, preserve unrelated hooks, and migrate that profile to the current installed path and event coverage. For runtime-oriented diagnosis, use `yam hook status --global --json --probe`; this separates configured hooks, discovery from a new bounded Codex app-server, and local entrypoint observations. The active Desktop session remains `not_measured` unless the host provides independent evidence. `yam hook enable <profile> --global --observe` records only a local hash-bound event observation; it is partial evidence and not host attestation. Restart Codex or open a new task after changing hooks.
106
110
 
107
111
  ## Install
108
112
 
package/dist/bin/yam.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
4
+ import { readMemoryDirectory } from '../lib/memory-records.js';
3
5
  import { runSecurityCommand } from '../lib/security-integration.js';
4
6
  import { createHash } from 'node:crypto';
5
7
  import fs from 'node:fs';
@@ -71,8 +73,8 @@ const INSTRUCTION_DUPLICATION_LIMITS = {
71
73
  minDirectiveChars: 40
72
74
  };
73
75
  const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
74
- const YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
75
- const YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
76
+ let YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
77
+ let YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
76
78
  const REQUIRED_PACK_SECTIONS = [
77
79
  'Product Direction',
78
80
  'UI Direction',
@@ -1944,6 +1946,8 @@ async function hook(args = []) {
1944
1946
  return hookUsage();
1945
1947
  if (subcommand === 'status')
1946
1948
  return hookStatus(args.slice(1));
1949
+ if (subcommand === 'migrate')
1950
+ return hookMigrate(args.slice(1));
1947
1951
  if (subcommand === 'enable')
1948
1952
  return hookEnable(args.slice(1));
1949
1953
  if (subcommand === 'disable')
@@ -1957,14 +1961,17 @@ function hookUsage() {
1957
1961
  console.log(`yam hook
1958
1962
 
1959
1963
  Usage:
1960
- yam hook status [--global|--project dir]
1961
- yam hook enable <lite|study-note> [--global|--project dir]
1964
+ yam hook status [--global|--project dir] [--probe] [--json]
1965
+ yam hook migrate [--global|--project dir]
1966
+ yam hook enable <lite|study-note> [--global|--project dir] [--observe]
1962
1967
  yam hook disable [lite|study-note] [--global|--project dir]
1963
1968
  yam hook run <lite|study-note>
1964
1969
 
1965
1970
  Notes:
1966
1971
  hooks are opt-in. lite is advisory-only; study-note adds a prompt reminder and a one-pass Stop completion gate.
1967
1972
  Hooks do not generate reports, run tmux, start subagents, or execute verification automatically.
1973
+ --probe queries hooks/list in a separate bounded Codex app-server; active session loading stays unknown.
1974
+ --observe opts into private local entrypoint observations without storing prompt content; not host attestation.
1968
1975
  `);
1969
1976
  }
1970
1977
  function parseHookArgs(args = []) {
@@ -1991,7 +1998,7 @@ function parseHookArgs(args = []) {
1991
1998
  }
1992
1999
  function hookPathFor(parsed) {
1993
2000
  if (parsed.mode === 'global')
1994
- return path.join(os.homedir(), '.codex', 'hooks.json');
2001
+ return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'hooks.json');
1995
2002
  return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
1996
2003
  }
1997
2004
  function isYamLiteHook(handler = {}) {
@@ -2003,12 +2010,34 @@ function isYamStudyNoteHook(handler = {}) {
2003
2010
  function isYamHookProfile(handler = {}, profile = 'lite') {
2004
2011
  return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
2005
2012
  }
2013
+ const CODEX_HOOK_EVENTS = new Set(['SessionStart', 'SessionEnd', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact', 'UserPromptSubmit', 'SubagentStart', 'SubagentStop', 'Stop', 'Interrupt']);
2014
+ function hookEventMap(config = {}) {
2015
+ const nested = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks) ? config.hooks : {};
2016
+ const events = { ...nested };
2017
+ for (const [event, entries] of Object.entries(config)) {
2018
+ if (CODEX_HOOK_EVENTS.has(event) && Array.isArray(entries)) {
2019
+ events[event] = [...(Array.isArray(events[event]) ? events[event] : []), ...entries];
2020
+ }
2021
+ }
2022
+ return events;
2023
+ }
2024
+ function canonicalHookConfig(config = {}) {
2025
+ if (config.hooks != null && (typeof config.hooks !== 'object' || Array.isArray(config.hooks)))
2026
+ throw new Error('hooks must be an event object; existing file preserved');
2027
+ const next = { ...config, hooks: hookEventMap(config) };
2028
+ for (const event of CODEX_HOOK_EVENTS) {
2029
+ if (event in config && !Array.isArray(config[event]))
2030
+ throw new Error('legacy hook event must be an array; existing file preserved');
2031
+ delete next[event];
2032
+ }
2033
+ return next;
2034
+ }
2006
2035
  function stripYamHooks(config = {}, profile = 'lite') {
2007
- const next = { ...config };
2008
- for (const event of Object.keys(next)) {
2009
- if (!Array.isArray(next[event]))
2036
+ const next = canonicalHookConfig(config);
2037
+ for (const event of Object.keys(next.hooks)) {
2038
+ if (!Array.isArray(next.hooks[event]))
2010
2039
  continue;
2011
- const entries = next[event];
2040
+ const entries = next.hooks[event];
2012
2041
  const keptEntries = [];
2013
2042
  for (const entry of entries) {
2014
2043
  const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
@@ -2017,9 +2046,9 @@ function stripYamHooks(config = {}, profile = 'lite') {
2017
2046
  keptEntries.push(rest);
2018
2047
  }
2019
2048
  if (keptEntries.length > 0)
2020
- next[event] = keptEntries;
2049
+ next.hooks[event] = keptEntries;
2021
2050
  else
2022
- delete next[event];
2051
+ delete next.hooks[event];
2023
2052
  }
2024
2053
  return next;
2025
2054
  }
@@ -2036,7 +2065,7 @@ function withYamHook(config = {}, profile = 'lite') {
2036
2065
  }
2037
2066
  ]
2038
2067
  };
2039
- next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
2068
+ next.hooks[event] = [...(Array.isArray(next.hooks[event]) ? next.hooks[event] : []), entry];
2040
2069
  }
2041
2070
  return next;
2042
2071
  }
@@ -2045,6 +2074,11 @@ async function hookStatus(args = []) {
2045
2074
  const target = hookPathFor(parsed);
2046
2075
  const loaded = await readHookConfig(target);
2047
2076
  if (loaded.error) {
2077
+ if (args.includes('--json')) {
2078
+ console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, configuration: 'unreadable', loaded: 'unknown', execution_observed: false, truth_status: 'partial' }, null, 2));
2079
+ process.exitCode = 1;
2080
+ return;
2081
+ }
2048
2082
  console.log('yam-lite hook: broken');
2049
2083
  console.log('yam-study-note hook: broken');
2050
2084
  console.log(` - hook config unreadable: ${loaded.error}`);
@@ -2055,10 +2089,30 @@ async function hookStatus(args = []) {
2055
2089
  }
2056
2090
  const lite = await inspectHookProfile(loaded.config, 'lite');
2057
2091
  const studyNote = await inspectHookProfile(loaded.config, 'study-note');
2092
+ const cwd = path.resolve(parsed.projectDir || process.cwd());
2093
+ const commands = { lite: lite.entries.map((entry) => String(entry.handler.command)), 'study-note': studyNote.entries.map((entry) => String(entry.handler.command)) };
2094
+ const host = args.includes('--probe')
2095
+ ? await probeHookDiscovery({ executable: await findExecutable('codex') || 'codex', cwd, commands, configPath: target })
2096
+ : { state: 'not_measured', active_session_loaded: 'not_measured', execution_observed: false, truth_status: 'partial' };
2097
+ const profiles = {};
2098
+ for (const [profile, health] of [['lite', lite], ['study-note', studyNote]]) {
2099
+ profiles[profile] = { configured: health.state, issues: health.issues, loaded: 'unknown',
2100
+ execution: await inspectHookObservation(cwd, profile, YAM_HOOK_ENTRYPOINT) };
2101
+ }
2102
+ if (args.includes('--json')) {
2103
+ console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, profiles, host, truth_status: 'partial' }, null, 2));
2104
+ if (lite.state === 'broken' || studyNote.state === 'broken')
2105
+ process.exitCode = 1;
2106
+ return;
2107
+ }
2058
2108
  printHookProfileStatus('lite', lite);
2059
2109
  printHookProfileStatus('study-note', studyNote);
2060
2110
  console.log(`scope: ${parsed.mode}`);
2061
2111
  console.log(`file: ${target}`);
2112
+ console.log('active Codex session loaded: unknown (configuration is not runtime evidence)');
2113
+ console.log(`host discovery: ${host.state}`);
2114
+ for (const profile of ['lite', 'study-note'])
2115
+ console.log(`yam-${profile} execution: ${profiles[profile].execution.state} (local entrypoint evidence, not host attestation)`);
2062
2116
  if (lite.state === 'broken' || studyNote.state === 'broken')
2063
2117
  process.exitCode = 1;
2064
2118
  }
@@ -2066,7 +2120,7 @@ function hookConfigHasYamLite(config = {}) {
2066
2120
  return hookConfigHasProfile(config, 'lite');
2067
2121
  }
2068
2122
  function hookConfigHasProfile(config = {}, profile = 'lite') {
2069
- return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
2123
+ return Object.values(hookEventMap(config)).some((entries) => Array.isArray(entries) && entries.some((entry) => {
2070
2124
  return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
2071
2125
  }));
2072
2126
  }
@@ -2078,7 +2132,7 @@ function hookEventsForProfile(profile = 'lite') {
2078
2132
  }
2079
2133
  function hookProfileEntries(config = {}, profile = 'lite') {
2080
2134
  const matches = [];
2081
- for (const [event, entries] of Object.entries(config)) {
2135
+ for (const [event, entries] of Object.entries(hookEventMap(config))) {
2082
2136
  if (!Array.isArray(entries))
2083
2137
  continue;
2084
2138
  for (const entry of entries) {
@@ -2142,6 +2196,8 @@ function splitHookCommand(command = '') {
2142
2196
  }
2143
2197
  function parseYamHookCommand(command = '', profile = 'lite') {
2144
2198
  const words = splitHookCommand(command);
2199
+ if (words.at(-1) === '--observe')
2200
+ words.pop();
2145
2201
  if (words.length === 5 && words[2] === 'hook' && words[3] === 'run' && words[4] === profile) {
2146
2202
  const nodeName = path.basename(words[0]).toLowerCase();
2147
2203
  if (/^node(?:\.exe)?$/.test(nodeName) && path.basename(words[1]).toLowerCase() === 'yam.js') {
@@ -2185,6 +2241,8 @@ async function inspectHookProfile(config = {}, profile = 'lite') {
2185
2241
  if (!entries.length)
2186
2242
  return { state: 'disabled', issues: [], entries };
2187
2243
  const issues = [];
2244
+ if (Object.keys(config).some((event) => CODEX_HOOK_EVENTS.has(event)))
2245
+ issues.push('legacy hook config requires the top-level hooks wrapper; rerun yam hook enable to migrate with backup');
2188
2246
  for (const event of hookEventsForProfile(profile)) {
2189
2247
  const count = entries.filter((entry) => entry.event === event).length;
2190
2248
  if (count === 0)
@@ -2213,6 +2271,9 @@ async function readHookConfig(target) {
2213
2271
  if (!value || Array.isArray(value) || typeof value !== 'object') {
2214
2272
  return { config: {}, exists: true, error: 'top-level value must be a JSON object' };
2215
2273
  }
2274
+ if (value.hooks != null && (typeof value.hooks !== 'object' || Array.isArray(value.hooks))) {
2275
+ return { config: {}, exists: true, error: 'hooks must be an event object' };
2276
+ }
2216
2277
  return { config: value, exists: true, error: '' };
2217
2278
  }
2218
2279
  catch (error) {
@@ -2233,8 +2294,41 @@ async function writeHookConfig(target, config) {
2233
2294
  throw error;
2234
2295
  }
2235
2296
  }
2297
+ async function hookMigrate(args = []) {
2298
+ const parsed = parseHookArgs(args);
2299
+ const target = hookPathFor(parsed);
2300
+ const loaded = await readHookConfig(target);
2301
+ if (loaded.error) {
2302
+ console.error('hook migration blocked: unreadable configuration');
2303
+ process.exitCode = 1;
2304
+ return;
2305
+ }
2306
+ if (!loaded.exists || !Object.keys(loaded.config).some((key) => CODEX_HOOK_EVENTS.has(key))) {
2307
+ console.log('hook migration: no legacy event layout found');
2308
+ return;
2309
+ }
2310
+ const before = await fsp.lstat(target);
2311
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
2312
+ throw new Error('hook migration blocked: unsafe config file');
2313
+ const original = await fsp.readFile(target, 'utf8');
2314
+ if (JSON.stringify(JSON.parse(original)) !== JSON.stringify(loaded.config))
2315
+ throw new Error('hook configuration changed; retry migration');
2316
+ const next = canonicalHookConfig(loaded.config);
2317
+ const backup = `${target}.yam-backup-${timestampId()}`;
2318
+ await fsp.copyFile(target, backup, fs.constants.COPYFILE_EXCL);
2319
+ const after = await fsp.lstat(target);
2320
+ if (after.dev !== before.dev || after.ino !== before.ino || after.mtimeMs !== before.mtimeMs
2321
+ || await fsp.readFile(target, 'utf8') !== original)
2322
+ throw new Error('hook configuration changed; backup preserved, retry migration');
2323
+ await writeHookConfig(target, next);
2324
+ console.log(`hook migration: wrapped event groups; handlers and metadata preserved\nbackup: ${backup}\nReview changed definitions in Codex /hooks before enabling execution.`);
2325
+ }
2236
2326
  async function hookEnable(args = []) {
2237
2327
  const parsed = parseHookArgs(args);
2328
+ const node = await selectHookNode();
2329
+ const observe = args.includes('--observe') ? ' --observe' : '';
2330
+ YAM_LITE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite${observe}`;
2331
+ YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note${observe}`;
2238
2332
  if (!['lite', 'study-note'].includes(parsed.profile)) {
2239
2333
  console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
2240
2334
  process.exitCode = 1;
@@ -2263,6 +2357,7 @@ async function hookEnable(args = []) {
2263
2357
  }
2264
2358
  await writeHookConfig(target, next);
2265
2359
  console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
2360
+ console.log(`node launcher: ${node.selection}; alias identity matched the current runtime (no PATH candidate executed)`);
2266
2361
  if (needsMigration)
2267
2362
  console.log(`migrated: ${existing.length} existing yam-${parsed.profile} handler(s) to the current command and event coverage`);
2268
2363
  console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
@@ -2283,7 +2378,7 @@ async function hookDisable(args = []) {
2283
2378
  return;
2284
2379
  }
2285
2380
  const next = stripYamHooks(current, parsed.profile);
2286
- if (Object.keys(next).length === 0) {
2381
+ if (Object.keys(next).length === 1 && Object.keys(next.hooks).length === 0) {
2287
2382
  await rmrf(target);
2288
2383
  }
2289
2384
  else {
@@ -2313,6 +2408,8 @@ async function hookRun(args = []) {
2313
2408
  }
2314
2409
  const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
2315
2410
  const cwd = String(input?.cwd || process.cwd());
2411
+ if (args.includes('--observe'))
2412
+ await recordHookObservation(cwd, profile, String(event), YAM_HOOK_ENTRYPOINT).catch(() => { });
2316
2413
  if (profile === 'study-note' && event === 'Stop') {
2317
2414
  console.log(JSON.stringify(await buildStudyNoteStopOutput(input, cwd)));
2318
2415
  return;
@@ -6079,7 +6176,7 @@ async function memory(args = []) {
6079
6176
  }
6080
6177
  if (subcommand === 'summary' || subcommand === 'summarize') {
6081
6178
  const parsed = parseMemoryArgs(args.slice(1));
6082
- return memorySummary(parsed.dir);
6179
+ return memorySummary(parsed.dir, { json: args.includes('--json') });
6083
6180
  }
6084
6181
  if (subcommand === 'resolve')
6085
6182
  return memoryResolve(args.slice(1));
@@ -6093,7 +6190,7 @@ Usage:
6093
6190
  yam memory init [dir]
6094
6191
  yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
6095
6192
  yam memory list [dir] [--json]
6096
- yam memory summary [dir]
6193
+ yam memory summary [dir] [--json]
6097
6194
  yam memory resolve [dir] <id> [--note <text>]
6098
6195
 
6099
6196
  Kinds:
@@ -6163,7 +6260,17 @@ async function memoryAdd(args = []) {
6163
6260
  console.log(`memory added: ${id}`);
6164
6261
  }
6165
6262
  async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
6166
- const records = await readMemoryRecords(targetDir);
6263
+ const report = await readMemoryRecords(targetDir);
6264
+ const records = report.records;
6265
+ if (!report.complete) {
6266
+ if (report.state !== 'missing')
6267
+ process.exitCode = 1;
6268
+ if (json)
6269
+ console.log(JSON.stringify(report, null, 2));
6270
+ else
6271
+ console.error(`memory read ${report.state}: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
6272
+ return;
6273
+ }
6167
6274
  if (json) {
6168
6275
  console.log(JSON.stringify(records, null, 2));
6169
6276
  return;
@@ -6178,9 +6285,18 @@ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
6178
6285
  console.log(` action: ${record.action}`);
6179
6286
  }
6180
6287
  }
6181
- async function memorySummary(targetDir = process.cwd()) {
6288
+ async function memorySummary(targetDir = process.cwd(), { json = false } = {}) {
6182
6289
  const dir = memoryDir(targetDir);
6183
- const records = await readMemoryRecords(targetDir);
6290
+ const report = await readMemoryRecords(targetDir);
6291
+ if (!report.complete) {
6292
+ process.exitCode = 1;
6293
+ if (json)
6294
+ console.log(JSON.stringify({ ...report, summary_written: false }, null, 2));
6295
+ else
6296
+ console.error(`memory summary blocked (${report.state}); existing summary preserved: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
6297
+ return;
6298
+ }
6299
+ const records = report.records;
6184
6300
  await fsp.mkdir(dir, { recursive: true });
6185
6301
  const active = records.filter((record) => record.status !== 'resolved');
6186
6302
  const resolved = records.filter((record) => record.status === 'resolved');
@@ -6212,7 +6328,10 @@ async function memorySummary(targetDir = process.cwd()) {
6212
6328
  }
6213
6329
  const target = path.join(dir, 'summary.md');
6214
6330
  await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
6215
- console.log(`memory summary written: ${target}`);
6331
+ if (json)
6332
+ console.log(JSON.stringify({ ...report, summary_written: true, path: target }, null, 2));
6333
+ else
6334
+ console.log(`memory summary written: ${target}`);
6216
6335
  }
6217
6336
  async function memoryResolve(args = []) {
6218
6337
  const parsed = parseMemoryArgs(args);
@@ -6267,23 +6386,7 @@ function memoryDir(targetDir = process.cwd()) {
6267
6386
  }
6268
6387
  async function readMemoryRecords(targetDir = process.cwd()) {
6269
6388
  const recordsDir = path.join(memoryDir(targetDir), 'records');
6270
- if (!await exists(recordsDir))
6271
- return [];
6272
- const entries = await fsp.readdir(recordsDir, { withFileTypes: true });
6273
- const records = [];
6274
- for (const entry of entries) {
6275
- if (!entry.isFile() || !entry.name.endsWith('.json'))
6276
- continue;
6277
- const file = path.join(recordsDir, entry.name);
6278
- try {
6279
- const record = await readJson(file);
6280
- records.push(record);
6281
- }
6282
- catch (error) {
6283
- records.push({ id: entry.name.replace(/\.json$/, ''), kind: 'invalid', status: 'invalid', summary: `invalid JSON: ${errorMessage(error)}` });
6284
- }
6285
- }
6286
- return records.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
6389
+ return readMemoryDirectory(recordsDir);
6287
6390
  }
6288
6391
  function timestampId() {
6289
6392
  const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.(\d{3})Z$/, '$1z');
@@ -129,7 +129,7 @@ export declare function createPlanReviewCanvas(input: {
129
129
  visual_correctness: string;
130
130
  implementation_correctness: string;
131
131
  };
132
- truth_status: "verified" | "partial";
132
+ truth_status: "partial" | "verified";
133
133
  }>;
134
134
  export declare function addPlanReviewComment(input: {
135
135
  root: string;
@@ -147,7 +147,7 @@ export declare function addPlanReviewComment(input: {
147
147
  visual_correctness: string;
148
148
  implementation_correctness: string;
149
149
  };
150
- truth_status: "verified" | "partial";
150
+ truth_status: "partial" | "verified";
151
151
  }>;
152
152
  export declare function closePlanReviewCanvas(input: {
153
153
  root: string;
@@ -165,7 +165,7 @@ export declare function closePlanReviewCanvas(input: {
165
165
  visual_correctness: string;
166
166
  implementation_correctness: string;
167
167
  };
168
- truth_status: "verified" | "partial";
168
+ truth_status: "partial" | "verified";
169
169
  }>;
170
170
  export declare function readPlanReviewSession(file: string): Promise<PlanReviewSession>;
171
171
  export declare function createDesignRevisionState(input: {
@@ -228,7 +228,7 @@ export declare function verifyFinalGalleryManifest(input: {
228
228
  implementation_correctness: string;
229
229
  };
230
230
  next_action: string;
231
- truth_status: "blocked" | "partial";
231
+ truth_status: "partial" | "blocked";
232
232
  }>;
233
233
  export declare function finalizeDesignProductionPhase(input: {
234
234
  root: string;
@@ -283,7 +283,7 @@ export declare function finalizeDesignProductionPhase(input: {
283
283
  server: false;
284
284
  background_service: false;
285
285
  };
286
- phase_evidence_status: "complete" | "bounded_stop" | "incomplete";
286
+ phase_evidence_status: "complete" | "incomplete" | "bounded_stop";
287
287
  ready: boolean;
288
288
  ready_to_claim_correctness: boolean;
289
289
  next_action: string;
@@ -301,6 +301,6 @@ export declare function verifyDesignProductionPhaseReceipt(input: {
301
301
  receipt_digest: string;
302
302
  ready_to_claim_correctness: boolean;
303
303
  next_action: string;
304
- truth_status: "blocked" | "partial";
304
+ truth_status: "partial" | "blocked";
305
305
  }>;
306
306
  export {};
@@ -22,6 +22,7 @@ export interface ExternalUpdatePaths {
22
22
  export interface ExternalUpdateDependencies {
23
23
  run?: (command: string, args: string[], options?: ExternalCommandOptions) => Promise<ExternalCommandResult> | ExternalCommandResult;
24
24
  fetchJson?: (url: string) => Promise<any>;
25
+ probeArtifact?: (url: string) => Promise<number>;
25
26
  now?: () => Date;
26
27
  homeDir?: string;
27
28
  env?: NodeJS.ProcessEnv;
@@ -33,7 +34,7 @@ export interface ExternalComponentCheck {
33
34
  installed_version: string;
34
35
  latest_version: string;
35
36
  update_available: boolean;
36
- status: 'up_to_date' | 'update_available' | 'not_installed' | 'check_failed';
37
+ status: 'up_to_date' | 'update_available' | 'not_installed' | 'pending_distribution' | 'check_failed';
37
38
  source: string;
38
39
  source_revision?: {
39
40
  local: string;
@@ -132,4 +133,8 @@ export declare function applyExternalUpdates(currentYamVersion: string, selectio
132
133
  truth_status: string;
133
134
  next_action: string;
134
135
  }>;
136
+ export declare class RegistryHttpError extends Error {
137
+ readonly status: number;
138
+ constructor(status: number, url: string);
139
+ }
135
140
  export {};