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 +10 -0
- package/COMMANDS.md +7 -2
- package/README.md +5 -1
- package/dist/bin/yam.js +141 -38
- 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/verification-closure.d.ts +1 -1
- package/package.json +6 -2
- package/src/bin/yam.ts +122 -35
- 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/yam.manifest.json +1 -1
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
|
-
|
|
75
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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" | "
|
|
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: "
|
|
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 {};
|