yam-flow 2.8.0 → 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/AGENTS.md CHANGED
@@ -23,6 +23,10 @@
23
23
  21. When relevant, include architecture hygiene in the Study Note: avoid dumping unrelated logic into `page.tsx`, one-off component CSS into `global.css`, or structured product data into broad DB `jsonb` blobs when existing project boundaries, scoped styles, typed tables, constraints, or indexes fit better.
24
24
  22. When an artifact changed, put `Next step` immediately after Study Note. Build it from a quick whole-process scan with current situation, forward outlook, a concrete critical opinion, improvement recommendations, and ordered fix-first then planned actions with evidence and ownership; do not merely rename remaining tasks.
25
25
 
26
+ ## Security check gate
27
+
28
+ Before commit, push, CI approval, or deployment, run the configured `yam security check` gate for the exact stage and record its Security check block. Use `references/security-check.md`; preserve every SEC explanation, evidence reference, and unresolved status. Missing required evidence or a note-write failure blocks the stage. Local hooks are bypassable; enforce protected merge/deployment with a trusted CI policy and required checks. Never present a validated supplied report as an independent vulnerability scan.
29
+
26
30
  ## Latest Update Trigger
27
31
 
28
32
  When the user says exactly `yam 최신 업데이트 사항 반영해줘` in a chat for this project, treat that phrase as explicit authorization to check and apply reviewed updates for yam, Scrapling, and Insane Search.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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
+
15
+ ## v2.8.1 - 2026-10-01
16
+
17
+ ### Security
18
+
19
+ - Add nine explained SEC areas, exact-stage evidence validation and safe append-only Security check notes. Missing, stale or unresolved evidence blocks the operation.
20
+ - Add project snapshot checks, reviewed source/policy pins, commit/push hooks and required CI security checks. Package lifecycle verification uses the same reviewed revision on Linux, macOS and Windows.
21
+ - Retain sanitized security notes in CI artifacts and require actual environment inspection for deployment. Document operator-run npm publication and its enforcement limits.
22
+
23
+ ### Verification
24
+
25
+ - Independent security audit, actual commit/push hooks, hosted security evidence and three-platform packaged lifecycle checks passed for the security implementation.
26
+
5
27
  ## v2.8.0 - 2026-09-01
6
28
 
7
29
  ### Added
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,12 +329,25 @@ 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.
344
+
345
+ ## Security check
346
+
347
+ - `yam security init`: create a private all-unverified draft and pin a reviewed policy; preserve existing files.
348
+ - `yam security check --stage commit|push|ci|deploy`: validate exact-target evidence and append Security check to Yam note; nonzero exit blocks.
349
+ - `yam security hooks enable`: explicitly install local pre-commit/pre-push hooks, preserving existing hooks.
350
+ - `yam security status`: show local configuration; remote protections remain not measured.
351
+ - `yam template security` / `yam template security-ci`: explained note and CI setup templates.
352
+
353
+ See `references/security-check.md` for stage arguments, trusted policy requirements and limitations. This validates supplied evidence; it does not automatically scan application vulnerabilities.
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,8 @@
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';
5
+ import { runSecurityCommand } from '../lib/security-integration.js';
3
6
  import { createHash } from 'node:crypto';
4
7
  import fs from 'node:fs';
5
8
  import fsp from 'node:fs/promises';
@@ -70,8 +73,8 @@ const INSTRUCTION_DUPLICATION_LIMITS = {
70
73
  minDirectiveChars: 40
71
74
  };
72
75
  const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
73
- const YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
74
- 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`;
75
78
  const REQUIRED_PACK_SECTIONS = [
76
79
  'Product Direction',
77
80
  'UI Direction',
@@ -145,6 +148,7 @@ Usage:
145
148
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
146
149
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
147
150
  yam study-note check [dir] [--report file|--text text] [--json]
151
+ yam security <init|check|hooks|status> [--stage commit|push|ci|deploy] [--json]
148
152
  yam next-step <report|verify> [--spec file|--receipt file] [--json]
149
153
  yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
150
154
  yam scout receipt <create|verify> [options]
@@ -170,7 +174,7 @@ Usage:
170
174
  yam safety [text...]
171
175
  yam memory <init|add|list|summary|resolve> [dir] [options]
172
176
  yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
173
- yam template <project|ueye|mission|proof|tuning>
177
+ yam template <project|ueye|mission|proof|tuning|security|security-ci>
174
178
  yam tune-log [dir]
175
179
  yam install [--dry-run] [--json] [--replace-user-skill name]
176
180
  yam uninstall
@@ -489,6 +493,7 @@ async function verify({ quiet = false } = {}) {
489
493
  'context-reuse.md',
490
494
  'markdown-management.md',
491
495
  'study-note.md',
496
+ 'security-check.md',
492
497
  'final-report.md',
493
498
  'token-budget-reporter.md',
494
499
  'memory.md'
@@ -507,7 +512,7 @@ async function verify({ quiet = false } = {}) {
507
512
  issues.push(`project template missing section: ${section}`);
508
513
  }
509
514
  }
510
- for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md']) {
515
+ for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md', 'security-check-note.md', 'security-gate.yml']) {
511
516
  if (!await exists(path.join(ROOT, 'templates', template)))
512
517
  issues.push(`missing template: ${template}`);
513
518
  }
@@ -1941,6 +1946,8 @@ async function hook(args = []) {
1941
1946
  return hookUsage();
1942
1947
  if (subcommand === 'status')
1943
1948
  return hookStatus(args.slice(1));
1949
+ if (subcommand === 'migrate')
1950
+ return hookMigrate(args.slice(1));
1944
1951
  if (subcommand === 'enable')
1945
1952
  return hookEnable(args.slice(1));
1946
1953
  if (subcommand === 'disable')
@@ -1954,14 +1961,17 @@ function hookUsage() {
1954
1961
  console.log(`yam hook
1955
1962
 
1956
1963
  Usage:
1957
- yam hook status [--global|--project dir]
1958
- 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]
1959
1967
  yam hook disable [lite|study-note] [--global|--project dir]
1960
1968
  yam hook run <lite|study-note>
1961
1969
 
1962
1970
  Notes:
1963
1971
  hooks are opt-in. lite is advisory-only; study-note adds a prompt reminder and a one-pass Stop completion gate.
1964
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.
1965
1975
  `);
1966
1976
  }
1967
1977
  function parseHookArgs(args = []) {
@@ -1988,7 +1998,7 @@ function parseHookArgs(args = []) {
1988
1998
  }
1989
1999
  function hookPathFor(parsed) {
1990
2000
  if (parsed.mode === 'global')
1991
- return path.join(os.homedir(), '.codex', 'hooks.json');
2001
+ return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'hooks.json');
1992
2002
  return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
1993
2003
  }
1994
2004
  function isYamLiteHook(handler = {}) {
@@ -2000,12 +2010,34 @@ function isYamStudyNoteHook(handler = {}) {
2000
2010
  function isYamHookProfile(handler = {}, profile = 'lite') {
2001
2011
  return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
2002
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
+ }
2003
2035
  function stripYamHooks(config = {}, profile = 'lite') {
2004
- const next = { ...config };
2005
- for (const event of Object.keys(next)) {
2006
- 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]))
2007
2039
  continue;
2008
- const entries = next[event];
2040
+ const entries = next.hooks[event];
2009
2041
  const keptEntries = [];
2010
2042
  for (const entry of entries) {
2011
2043
  const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
@@ -2014,9 +2046,9 @@ function stripYamHooks(config = {}, profile = 'lite') {
2014
2046
  keptEntries.push(rest);
2015
2047
  }
2016
2048
  if (keptEntries.length > 0)
2017
- next[event] = keptEntries;
2049
+ next.hooks[event] = keptEntries;
2018
2050
  else
2019
- delete next[event];
2051
+ delete next.hooks[event];
2020
2052
  }
2021
2053
  return next;
2022
2054
  }
@@ -2033,7 +2065,7 @@ function withYamHook(config = {}, profile = 'lite') {
2033
2065
  }
2034
2066
  ]
2035
2067
  };
2036
- next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
2068
+ next.hooks[event] = [...(Array.isArray(next.hooks[event]) ? next.hooks[event] : []), entry];
2037
2069
  }
2038
2070
  return next;
2039
2071
  }
@@ -2042,6 +2074,11 @@ async function hookStatus(args = []) {
2042
2074
  const target = hookPathFor(parsed);
2043
2075
  const loaded = await readHookConfig(target);
2044
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
+ }
2045
2082
  console.log('yam-lite hook: broken');
2046
2083
  console.log('yam-study-note hook: broken');
2047
2084
  console.log(` - hook config unreadable: ${loaded.error}`);
@@ -2052,10 +2089,30 @@ async function hookStatus(args = []) {
2052
2089
  }
2053
2090
  const lite = await inspectHookProfile(loaded.config, 'lite');
2054
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
+ }
2055
2108
  printHookProfileStatus('lite', lite);
2056
2109
  printHookProfileStatus('study-note', studyNote);
2057
2110
  console.log(`scope: ${parsed.mode}`);
2058
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)`);
2059
2116
  if (lite.state === 'broken' || studyNote.state === 'broken')
2060
2117
  process.exitCode = 1;
2061
2118
  }
@@ -2063,7 +2120,7 @@ function hookConfigHasYamLite(config = {}) {
2063
2120
  return hookConfigHasProfile(config, 'lite');
2064
2121
  }
2065
2122
  function hookConfigHasProfile(config = {}, profile = 'lite') {
2066
- 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) => {
2067
2124
  return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
2068
2125
  }));
2069
2126
  }
@@ -2075,7 +2132,7 @@ function hookEventsForProfile(profile = 'lite') {
2075
2132
  }
2076
2133
  function hookProfileEntries(config = {}, profile = 'lite') {
2077
2134
  const matches = [];
2078
- for (const [event, entries] of Object.entries(config)) {
2135
+ for (const [event, entries] of Object.entries(hookEventMap(config))) {
2079
2136
  if (!Array.isArray(entries))
2080
2137
  continue;
2081
2138
  for (const entry of entries) {
@@ -2139,6 +2196,8 @@ function splitHookCommand(command = '') {
2139
2196
  }
2140
2197
  function parseYamHookCommand(command = '', profile = 'lite') {
2141
2198
  const words = splitHookCommand(command);
2199
+ if (words.at(-1) === '--observe')
2200
+ words.pop();
2142
2201
  if (words.length === 5 && words[2] === 'hook' && words[3] === 'run' && words[4] === profile) {
2143
2202
  const nodeName = path.basename(words[0]).toLowerCase();
2144
2203
  if (/^node(?:\.exe)?$/.test(nodeName) && path.basename(words[1]).toLowerCase() === 'yam.js') {
@@ -2182,6 +2241,8 @@ async function inspectHookProfile(config = {}, profile = 'lite') {
2182
2241
  if (!entries.length)
2183
2242
  return { state: 'disabled', issues: [], entries };
2184
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');
2185
2246
  for (const event of hookEventsForProfile(profile)) {
2186
2247
  const count = entries.filter((entry) => entry.event === event).length;
2187
2248
  if (count === 0)
@@ -2210,6 +2271,9 @@ async function readHookConfig(target) {
2210
2271
  if (!value || Array.isArray(value) || typeof value !== 'object') {
2211
2272
  return { config: {}, exists: true, error: 'top-level value must be a JSON object' };
2212
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
+ }
2213
2277
  return { config: value, exists: true, error: '' };
2214
2278
  }
2215
2279
  catch (error) {
@@ -2230,8 +2294,41 @@ async function writeHookConfig(target, config) {
2230
2294
  throw error;
2231
2295
  }
2232
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
+ }
2233
2326
  async function hookEnable(args = []) {
2234
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}`;
2235
2332
  if (!['lite', 'study-note'].includes(parsed.profile)) {
2236
2333
  console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
2237
2334
  process.exitCode = 1;
@@ -2260,6 +2357,7 @@ async function hookEnable(args = []) {
2260
2357
  }
2261
2358
  await writeHookConfig(target, next);
2262
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)`);
2263
2361
  if (needsMigration)
2264
2362
  console.log(`migrated: ${existing.length} existing yam-${parsed.profile} handler(s) to the current command and event coverage`);
2265
2363
  console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
@@ -2280,7 +2378,7 @@ async function hookDisable(args = []) {
2280
2378
  return;
2281
2379
  }
2282
2380
  const next = stripYamHooks(current, parsed.profile);
2283
- if (Object.keys(next).length === 0) {
2381
+ if (Object.keys(next).length === 1 && Object.keys(next.hooks).length === 0) {
2284
2382
  await rmrf(target);
2285
2383
  }
2286
2384
  else {
@@ -2310,6 +2408,8 @@ async function hookRun(args = []) {
2310
2408
  }
2311
2409
  const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
2312
2410
  const cwd = String(input?.cwd || process.cwd());
2411
+ if (args.includes('--observe'))
2412
+ await recordHookObservation(cwd, profile, String(event), YAM_HOOK_ENTRYPOINT).catch(() => { });
2313
2413
  if (profile === 'study-note' && event === 'Stop') {
2314
2414
  console.log(JSON.stringify(await buildStudyNoteStopOutput(input, cwd)));
2315
2415
  return;
@@ -6038,11 +6138,13 @@ async function printTemplate(name = '') {
6038
6138
  mission: 'mission-plan.md',
6039
6139
  proof: 'runtime-proof.md',
6040
6140
  runtime: 'runtime-proof.md',
6041
- tuning: 'tuning-log.md'
6141
+ tuning: 'tuning-log.md',
6142
+ security: 'security-check-note.md',
6143
+ 'security-ci': 'security-gate.yml'
6042
6144
  };
6043
6145
  const file = map[key];
6044
6146
  if (!file) {
6045
- console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning>');
6147
+ console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning|security|security-ci>');
6046
6148
  process.exitCode = 1;
6047
6149
  return;
6048
6150
  }
@@ -6074,7 +6176,7 @@ async function memory(args = []) {
6074
6176
  }
6075
6177
  if (subcommand === 'summary' || subcommand === 'summarize') {
6076
6178
  const parsed = parseMemoryArgs(args.slice(1));
6077
- return memorySummary(parsed.dir);
6179
+ return memorySummary(parsed.dir, { json: args.includes('--json') });
6078
6180
  }
6079
6181
  if (subcommand === 'resolve')
6080
6182
  return memoryResolve(args.slice(1));
@@ -6088,7 +6190,7 @@ Usage:
6088
6190
  yam memory init [dir]
6089
6191
  yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
6090
6192
  yam memory list [dir] [--json]
6091
- yam memory summary [dir]
6193
+ yam memory summary [dir] [--json]
6092
6194
  yam memory resolve [dir] <id> [--note <text>]
6093
6195
 
6094
6196
  Kinds:
@@ -6158,7 +6260,17 @@ async function memoryAdd(args = []) {
6158
6260
  console.log(`memory added: ${id}`);
6159
6261
  }
6160
6262
  async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
6161
- 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
+ }
6162
6274
  if (json) {
6163
6275
  console.log(JSON.stringify(records, null, 2));
6164
6276
  return;
@@ -6173,9 +6285,18 @@ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
6173
6285
  console.log(` action: ${record.action}`);
6174
6286
  }
6175
6287
  }
6176
- async function memorySummary(targetDir = process.cwd()) {
6288
+ async function memorySummary(targetDir = process.cwd(), { json = false } = {}) {
6177
6289
  const dir = memoryDir(targetDir);
6178
- 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;
6179
6300
  await fsp.mkdir(dir, { recursive: true });
6180
6301
  const active = records.filter((record) => record.status !== 'resolved');
6181
6302
  const resolved = records.filter((record) => record.status === 'resolved');
@@ -6207,7 +6328,10 @@ async function memorySummary(targetDir = process.cwd()) {
6207
6328
  }
6208
6329
  const target = path.join(dir, 'summary.md');
6209
6330
  await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
6210
- 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}`);
6211
6335
  }
6212
6336
  async function memoryResolve(args = []) {
6213
6337
  const parsed = parseMemoryArgs(args);
@@ -6262,23 +6386,7 @@ function memoryDir(targetDir = process.cwd()) {
6262
6386
  }
6263
6387
  async function readMemoryRecords(targetDir = process.cwd()) {
6264
6388
  const recordsDir = path.join(memoryDir(targetDir), 'records');
6265
- if (!await exists(recordsDir))
6266
- return [];
6267
- const entries = await fsp.readdir(recordsDir, { withFileTypes: true });
6268
- const records = [];
6269
- for (const entry of entries) {
6270
- if (!entry.isFile() || !entry.name.endsWith('.json'))
6271
- continue;
6272
- const file = path.join(recordsDir, entry.name);
6273
- try {
6274
- const record = await readJson(file);
6275
- records.push(record);
6276
- }
6277
- catch (error) {
6278
- records.push({ id: entry.name.replace(/\.json$/, ''), kind: 'invalid', status: 'invalid', summary: `invalid JSON: ${errorMessage(error)}` });
6279
- }
6280
- }
6281
- return records.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
6389
+ return readMemoryDirectory(recordsDir);
6282
6390
  }
6283
6391
  function timestampId() {
6284
6392
  const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.(\d{3})Z$/, '$1z');
@@ -6309,6 +6417,7 @@ function showRequestedHelp(args) {
6309
6417
  return true;
6310
6418
  }
6311
6419
  const commandUsage = {
6420
+ security: () => console.log('yam security <init|check|hooks enable|status> [--stage commit|push|ci|deploy] [--policy file --policy-digest sha256 --evidence file --note file]\nSee references/security-check.md for exact bindings and CI/deploy trust configuration.'),
6312
6421
  context: contextUsage,
6313
6422
  cleanup: cleanupUsage,
6314
6423
  'study-note': studyNoteUsage,
@@ -6336,6 +6445,17 @@ async function main() {
6336
6445
  return;
6337
6446
  if (command === 'help')
6338
6447
  return usage();
6448
+ if (command === 'security') {
6449
+ if (args.length === 1 || ['help', '--help', '-h'].includes(args[1])) {
6450
+ console.log('yam security <init|check|hooks enable|status> [--stage commit|push|ci|deploy] [--policy file --policy-digest sha256 --evidence file --note file]\nSee references/security-check.md for exact bindings and CI/deploy trust configuration.');
6451
+ return;
6452
+ }
6453
+ const result = await runSecurityCommand(args.slice(1), { cwd: process.cwd(), cliPath: fileURLToPath(import.meta.url) });
6454
+ console.log(JSON.stringify(result, null, 2));
6455
+ if (!result.ok)
6456
+ process.exitCode = 1;
6457
+ return;
6458
+ }
6339
6459
  if (command === 'install')
6340
6460
  return install(args.slice(1));
6341
6461
  if (command === 'uninstall')
@@ -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 {};