yam-flow 2.8.1 → 2.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +25 -0
- package/COMMANDS.md +7 -2
- package/README.md +12 -3
- package/dist/bin/yam.js +171 -76
- package/dist/lib/design-production.d.ts +6 -6
- package/dist/lib/external-updates.d.ts +6 -1
- package/dist/lib/external-updates.js +93 -6
- package/dist/lib/hook-readiness.d.ts +54 -0
- package/dist/lib/hook-readiness.js +259 -0
- package/dist/lib/memory-records.d.ts +29 -0
- package/dist/lib/memory-records.js +92 -0
- package/dist/lib/study-note-scope.d.ts +8 -0
- package/dist/lib/study-note-scope.js +165 -0
- package/dist/lib/verification-closure.d.ts +1 -1
- package/package.json +8 -3
- package/references/context-reuse.md +1 -0
- package/references/final-report.md +3 -1
- package/references/next-step.md +2 -0
- package/references/security-check.md +2 -0
- package/references/study-note.md +4 -0
- package/skills/deep/SKILL.md +1 -0
- package/skills/mission/SKILL.md +1 -0
- package/skills/question/SKILL.md +1 -0
- package/skills/quick/SKILL.md +2 -1
- package/skills/scout/SKILL.md +1 -0
- package/skills/ueye/SKILL.md +1 -0
- package/src/bin/yam.ts +151 -74
- package/src/lib/external-updates.ts +87 -6
- package/src/lib/hook-readiness.ts +215 -0
- package/src/lib/memory-records.ts +109 -0
- package/src/lib/study-note-scope.ts +134 -0
- package/yam.manifest.json +4 -3
package/AGENTS.md
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
19. Treat security as the first project lens: identify sensitive surfaces early and guide safer defaults before convenience.
|
|
22
22
|
20. Keep final reports short and concrete, but always include remaining tasks or state that none are meaningful.
|
|
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
|
-
22. When an artifact changed, put `Next step` immediately after Study Note.
|
|
24
|
+
22. When an artifact changed, put `Next step` immediately after Study Note. Ordinary advisory work may use a short concrete action or state that none remains. For risky changes, explicit strict reports and structured receipts, 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. Keep report validation separate from actual security verification.
|
|
25
|
+
23. Work on the current branch by default. Create or switch branches, create worktrees, or create pull requests only when the user explicitly requests that action or an applicable project rule explicitly requires it. A commit/push request, release version bump, route selection, branch-name prefix, or branch protection alone is not authorization. If protection blocks the requested operation, report the blocker and ask for direction; do not silently choose a PR workflow, bypass checks, or change protection settings.
|
|
25
26
|
|
|
26
27
|
## Security check gate
|
|
27
28
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to yam-flow are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.8.3 - 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
|
|
9
|
+
- Study Note hooks compare bounded hash-only turn baselines instead of repeatedly treating pre-existing dirty files as new work; already-dirty edits, staging, commits, renames and deletions remain visible.
|
|
10
|
+
- Ordinary work uses advisory completion with concise notes; risky intent or explicit strict policy uses one bounded correction pass only when selected artifacts changed. Missing session or stale scope stays partial and requests manual inspection.
|
|
11
|
+
- Known generated outputs are excluded before content reads, with project include rules taking precedence over excludes; independent commit/push/deploy security gates remain unchanged.
|
|
12
|
+
- Standalone bold report headings are accepted, correction messages list missing fields, and migration documentation no longer triggers database hygiene based only on its name.
|
|
13
|
+
- Report guidance and Quick route now distinguish ordinary advisory handoffs from detailed strict reports.
|
|
14
|
+
- All routes and lite hook guidance keep the current branch by default; new/switched branches, worktrees and PRs require explicit user direction or applicable project rules. Protection blockers are reported without silently changing workflow or settings.
|
|
15
|
+
|
|
16
|
+
### Verification
|
|
17
|
+
|
|
18
|
+
- Turn-scope, generated/include precedence, advisory/strict, stale/corrupt baseline and symlink safety fixtures; hook latency and packaged CLI regressions.
|
|
19
|
+
|
|
20
|
+
## v2.8.2 - 2026-10-02
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- External yam update checks distinguish exact-version npm distribution propagation (`pending_distribution`) from network, authorization, or metadata failures.
|
|
27
|
+
- Memory listing and summary preserve the difference between missing, empty, invalid, unreadable, and incomplete record storage; incomplete reads cannot overwrite an existing summary.
|
|
28
|
+
- Added bounded Markdown skill-content checks for frontmatter identity, symlinks, personal absolute paths, download-to-shell patterns, and high-signal secrets.
|
|
29
|
+
|
|
5
30
|
## v2.8.1 - 2026-10-01
|
|
6
31
|
|
|
7
32
|
### 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
|
@@ -39,13 +39,14 @@ See `ROADMAP.md` for remaining implementation stages.
|
|
|
39
39
|
- No automatic Team routing.
|
|
40
40
|
- No automatic proof loops.
|
|
41
41
|
- No automatic tmux.
|
|
42
|
+
- Keep the current branch by default. New/switched branches, worktrees and PRs require explicit user direction or an applicable project rule; commit/push, release bumps and protection rules alone do not authorize them. Report protection blockers before changing workflow or settings.
|
|
42
43
|
- No false completion claims: verification, cleanup, and visual checks must match actual evidence.
|
|
43
44
|
- `yam` is not lightweight-only; it is progressive: fast direction, broader judgment when useful, stronger proof as scope/risk grows.
|
|
44
45
|
- Every route should check project direction and use the L0-L5 Verification Ladder honestly.
|
|
45
46
|
- Small work stays small, but serious work is allowed to become serious.
|
|
46
47
|
- Security-sensitive work is never treated as ordinary polish.
|
|
47
48
|
- When code, config, release metadata, documentation, or project artifacts changed, final reports include a Study Note even if no yam skill was invoked.
|
|
48
|
-
- A changed-artifact report puts `Next step` immediately after Study Note.
|
|
49
|
+
- A changed-artifact report puts `Next step` immediately after Study Note. Ordinary advisory work may state the next concrete action or no remaining work. Strict reports rescan the whole process, state situation/outlook, add critical opinion and recommendations, then order fix-first work before planned work.
|
|
49
50
|
- Useful Study Notes explain what the changed code or artifact does, what role it has, what was wrong or missing, what changed, what behavior is expected, one small syntax/structure insight, what was verified, and what remains uncertain.
|
|
50
51
|
- Research and reference scans should keep source boundaries clear, then rework good ideas into yam style.
|
|
51
52
|
- Token economy is part of quality.
|
|
@@ -74,6 +75,8 @@ yam memory add . --kind lesson --summary "Keep UI checks visual before declaring
|
|
|
74
75
|
yam memory summary .
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
`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.
|
|
79
|
+
|
|
77
80
|
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
81
|
|
|
79
82
|
## Lite Hook
|
|
@@ -91,6 +94,8 @@ It only adds short advisory context. It does not run verification, tmux, browser
|
|
|
91
94
|
|
|
92
95
|
## Study Note Hook
|
|
93
96
|
|
|
97
|
+
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.
|
|
98
|
+
|
|
94
99
|
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
100
|
|
|
96
101
|
```bash
|
|
@@ -98,11 +103,15 @@ yam hook enable study-note --global
|
|
|
98
103
|
yam hook status --global
|
|
99
104
|
```
|
|
100
105
|
|
|
101
|
-
The profile installs `UserPromptSubmit` and `Stop` handlers.
|
|
106
|
+
The profile installs `UserPromptSubmit` and `Stop` handlers. The prompt saves a bounded, hash-only baseline of index and dirty-file content in a private OS temporary directory, keyed by canonical repository and session (and checked against a turn ID when supplied). Stop inspects only subsequent changes, including edits to already-dirty files, staging, commits, renames and deletions. Missing/stale scope stays partial with manual-inspection guidance; it never means a clean project.
|
|
107
|
+
|
|
108
|
+
Ordinary hook work is advisory: a short Study Note (role, verification and limits) followed by Next step, including “no remaining work”, is sufficient. An explicit `study-note strict` request, project strict policy, or commit/push/deploy/DB/security intent enables the detailed contract **only when selected artifacts changed**. Deep/Mission names alone do not enable strict. Strict requests at most one correction; messages list missing fields. `yam study-note check` remains an explicit strict whole-tree check for compatibility. Neither mode performs security verification; commit/push/deploy security gates remain separate and unchanged.
|
|
109
|
+
|
|
110
|
+
Known generated `.yam/mission/`, `.yam/security/`, `.yam/logs/`, `.yam/screenshots/`, `dist/` and `.tgz` outputs are excluded from automatic turn reporting. This is not a blanket `.yam` exclusion. Optional `.yam/study-note.json` accepts `mode: "advisory" | "strict"`, `include` and `exclude` arrays of repository-relative exact paths or `directory/**` prefixes; include wins. Example: `{"include":[".yam/security/review.json"],"exclude":["fixtures/generated/**"]}`. Baselines expire after 24 hours and contain paths/digests, not source text or prompts. Without a turn ID the latest baseline for the session is used; overlapping turns cannot be independently attributed. Bounded snapshots (4,000 files, 2 MiB dirty content) fall back to manual inspection. The hook never writes the report, changes global configuration or bypasses a security gate.
|
|
102
111
|
|
|
103
112
|
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
113
|
|
|
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.
|
|
114
|
+
`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
115
|
|
|
107
116
|
## Install
|
|
108
117
|
|
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 { studyNoteTurnScope } from '../lib/study-note-scope.js';
|
|
4
|
+
import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
|
|
5
|
+
import { readMemoryDirectory } from '../lib/memory-records.js';
|
|
3
6
|
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
4
7
|
import { createHash } from 'node:crypto';
|
|
5
8
|
import fs from 'node:fs';
|
|
@@ -71,8 +74,8 @@ const INSTRUCTION_DUPLICATION_LIMITS = {
|
|
|
71
74
|
minDirectiveChars: 40
|
|
72
75
|
};
|
|
73
76
|
const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
let YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
|
|
78
|
+
let YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
|
|
76
79
|
const REQUIRED_PACK_SECTIONS = [
|
|
77
80
|
'Product Direction',
|
|
78
81
|
'UI Direction',
|
|
@@ -932,8 +935,8 @@ async function studyNoteCheck(args = []) {
|
|
|
932
935
|
if (result.blockers.length)
|
|
933
936
|
process.exitCode = 1;
|
|
934
937
|
}
|
|
935
|
-
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
|
|
936
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
938
|
+
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided', scope = null) {
|
|
939
|
+
const changedFileDetection = scope || gitChangedFileSnapshot(dir);
|
|
937
940
|
const changedFiles = changedFileDetection.files;
|
|
938
941
|
const changeScopeAvailable = changedFileDetection.available;
|
|
939
942
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
@@ -964,6 +967,14 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
964
967
|
for (const requirement of hygieneRequirements) {
|
|
965
968
|
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
966
969
|
}
|
|
970
|
+
if (scope?.mode === 'advisory') {
|
|
971
|
+
const ordinary = new Set(['changed_files', 'study_note_present', 'role_or_responsibility', 'verification', 'limits_or_uncertainty', 'next_step_present', 'single_study_note_next_step_pair', 'study_note_then_next_step']);
|
|
972
|
+
for (const check of checks)
|
|
973
|
+
if (!ordinary.has(check.id)) {
|
|
974
|
+
check.status = 'skipped';
|
|
975
|
+
check.truth_status = 'skipped';
|
|
976
|
+
}
|
|
977
|
+
}
|
|
967
978
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
968
979
|
const result = {
|
|
969
980
|
schema: 'yam.study-note-guard.v1',
|
|
@@ -1052,7 +1063,7 @@ function hasNextStepMarker(text = '') {
|
|
|
1052
1063
|
}
|
|
1053
1064
|
function reportSectionMarkers(text = '') {
|
|
1054
1065
|
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1055
|
-
const trimmed = line.trim();
|
|
1066
|
+
const trimmed = line.trim().replace(/^\*\*(.*?)\*\*\s*:?[ \t]*$/, '$1');
|
|
1056
1067
|
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1057
1068
|
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1058
1069
|
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
@@ -1176,7 +1187,7 @@ function studyNoteHygieneRequirements(files = []) {
|
|
|
1176
1187
|
if (files.some((file) => /(^|\/)global\.css$/i.test(file))) {
|
|
1177
1188
|
requirements.push({ id: 'global_css_hygiene', terms: ['global.css', 'token', 'component', 'scoped', 'module', 'utility'], note: 'Study Note should say whether CSS belongs globally or should move to tokens/scoped/component styles' });
|
|
1178
1189
|
}
|
|
1179
|
-
if (files.some((file) => /(
|
|
1190
|
+
if (files.some((file) => /\.(sql|prisma)$/i.test(file) || /(^|\/)(migrations?|schema|database|drizzle|supabase)\//i.test(file) && /\.(ts|js)$/i.test(file))) {
|
|
1180
1191
|
requirements.push({ id: 'jsonb_hygiene', terms: ['jsonb', 'column', 'table', 'constraint', 'index', 'schema', 'relation'], note: 'Study Note should say whether structured product data belongs in typed columns/tables/constraints/indexes instead of broad DB jsonb' });
|
|
1181
1192
|
}
|
|
1182
1193
|
return requirements;
|
|
@@ -1944,6 +1955,8 @@ async function hook(args = []) {
|
|
|
1944
1955
|
return hookUsage();
|
|
1945
1956
|
if (subcommand === 'status')
|
|
1946
1957
|
return hookStatus(args.slice(1));
|
|
1958
|
+
if (subcommand === 'migrate')
|
|
1959
|
+
return hookMigrate(args.slice(1));
|
|
1947
1960
|
if (subcommand === 'enable')
|
|
1948
1961
|
return hookEnable(args.slice(1));
|
|
1949
1962
|
if (subcommand === 'disable')
|
|
@@ -1957,14 +1970,17 @@ function hookUsage() {
|
|
|
1957
1970
|
console.log(`yam hook
|
|
1958
1971
|
|
|
1959
1972
|
Usage:
|
|
1960
|
-
yam hook status [--global|--project dir]
|
|
1961
|
-
yam hook
|
|
1973
|
+
yam hook status [--global|--project dir] [--probe] [--json]
|
|
1974
|
+
yam hook migrate [--global|--project dir]
|
|
1975
|
+
yam hook enable <lite|study-note> [--global|--project dir] [--observe]
|
|
1962
1976
|
yam hook disable [lite|study-note] [--global|--project dir]
|
|
1963
1977
|
yam hook run <lite|study-note>
|
|
1964
1978
|
|
|
1965
1979
|
Notes:
|
|
1966
1980
|
hooks are opt-in. lite is advisory-only; study-note adds a prompt reminder and a one-pass Stop completion gate.
|
|
1967
1981
|
Hooks do not generate reports, run tmux, start subagents, or execute verification automatically.
|
|
1982
|
+
--probe queries hooks/list in a separate bounded Codex app-server; active session loading stays unknown.
|
|
1983
|
+
--observe opts into private local entrypoint observations without storing prompt content; not host attestation.
|
|
1968
1984
|
`);
|
|
1969
1985
|
}
|
|
1970
1986
|
function parseHookArgs(args = []) {
|
|
@@ -1991,7 +2007,7 @@ function parseHookArgs(args = []) {
|
|
|
1991
2007
|
}
|
|
1992
2008
|
function hookPathFor(parsed) {
|
|
1993
2009
|
if (parsed.mode === 'global')
|
|
1994
|
-
return path.join(os.homedir(), '.codex', 'hooks.json');
|
|
2010
|
+
return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'hooks.json');
|
|
1995
2011
|
return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
|
|
1996
2012
|
}
|
|
1997
2013
|
function isYamLiteHook(handler = {}) {
|
|
@@ -2003,12 +2019,34 @@ function isYamStudyNoteHook(handler = {}) {
|
|
|
2003
2019
|
function isYamHookProfile(handler = {}, profile = 'lite') {
|
|
2004
2020
|
return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
|
|
2005
2021
|
}
|
|
2022
|
+
const CODEX_HOOK_EVENTS = new Set(['SessionStart', 'SessionEnd', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact', 'UserPromptSubmit', 'SubagentStart', 'SubagentStop', 'Stop', 'Interrupt']);
|
|
2023
|
+
function hookEventMap(config = {}) {
|
|
2024
|
+
const nested = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks) ? config.hooks : {};
|
|
2025
|
+
const events = { ...nested };
|
|
2026
|
+
for (const [event, entries] of Object.entries(config)) {
|
|
2027
|
+
if (CODEX_HOOK_EVENTS.has(event) && Array.isArray(entries)) {
|
|
2028
|
+
events[event] = [...(Array.isArray(events[event]) ? events[event] : []), ...entries];
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return events;
|
|
2032
|
+
}
|
|
2033
|
+
function canonicalHookConfig(config = {}) {
|
|
2034
|
+
if (config.hooks != null && (typeof config.hooks !== 'object' || Array.isArray(config.hooks)))
|
|
2035
|
+
throw new Error('hooks must be an event object; existing file preserved');
|
|
2036
|
+
const next = { ...config, hooks: hookEventMap(config) };
|
|
2037
|
+
for (const event of CODEX_HOOK_EVENTS) {
|
|
2038
|
+
if (event in config && !Array.isArray(config[event]))
|
|
2039
|
+
throw new Error('legacy hook event must be an array; existing file preserved');
|
|
2040
|
+
delete next[event];
|
|
2041
|
+
}
|
|
2042
|
+
return next;
|
|
2043
|
+
}
|
|
2006
2044
|
function stripYamHooks(config = {}, profile = 'lite') {
|
|
2007
|
-
const next =
|
|
2008
|
-
for (const event of Object.keys(next)) {
|
|
2009
|
-
if (!Array.isArray(next[event]))
|
|
2045
|
+
const next = canonicalHookConfig(config);
|
|
2046
|
+
for (const event of Object.keys(next.hooks)) {
|
|
2047
|
+
if (!Array.isArray(next.hooks[event]))
|
|
2010
2048
|
continue;
|
|
2011
|
-
const entries = next[event];
|
|
2049
|
+
const entries = next.hooks[event];
|
|
2012
2050
|
const keptEntries = [];
|
|
2013
2051
|
for (const entry of entries) {
|
|
2014
2052
|
const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
|
|
@@ -2017,9 +2055,9 @@ function stripYamHooks(config = {}, profile = 'lite') {
|
|
|
2017
2055
|
keptEntries.push(rest);
|
|
2018
2056
|
}
|
|
2019
2057
|
if (keptEntries.length > 0)
|
|
2020
|
-
next[event] = keptEntries;
|
|
2058
|
+
next.hooks[event] = keptEntries;
|
|
2021
2059
|
else
|
|
2022
|
-
delete next[event];
|
|
2060
|
+
delete next.hooks[event];
|
|
2023
2061
|
}
|
|
2024
2062
|
return next;
|
|
2025
2063
|
}
|
|
@@ -2036,7 +2074,7 @@ function withYamHook(config = {}, profile = 'lite') {
|
|
|
2036
2074
|
}
|
|
2037
2075
|
]
|
|
2038
2076
|
};
|
|
2039
|
-
next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
|
|
2077
|
+
next.hooks[event] = [...(Array.isArray(next.hooks[event]) ? next.hooks[event] : []), entry];
|
|
2040
2078
|
}
|
|
2041
2079
|
return next;
|
|
2042
2080
|
}
|
|
@@ -2045,6 +2083,11 @@ async function hookStatus(args = []) {
|
|
|
2045
2083
|
const target = hookPathFor(parsed);
|
|
2046
2084
|
const loaded = await readHookConfig(target);
|
|
2047
2085
|
if (loaded.error) {
|
|
2086
|
+
if (args.includes('--json')) {
|
|
2087
|
+
console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, configuration: 'unreadable', loaded: 'unknown', execution_observed: false, truth_status: 'partial' }, null, 2));
|
|
2088
|
+
process.exitCode = 1;
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2048
2091
|
console.log('yam-lite hook: broken');
|
|
2049
2092
|
console.log('yam-study-note hook: broken');
|
|
2050
2093
|
console.log(` - hook config unreadable: ${loaded.error}`);
|
|
@@ -2055,10 +2098,30 @@ async function hookStatus(args = []) {
|
|
|
2055
2098
|
}
|
|
2056
2099
|
const lite = await inspectHookProfile(loaded.config, 'lite');
|
|
2057
2100
|
const studyNote = await inspectHookProfile(loaded.config, 'study-note');
|
|
2101
|
+
const cwd = path.resolve(parsed.projectDir || process.cwd());
|
|
2102
|
+
const commands = { lite: lite.entries.map((entry) => String(entry.handler.command)), 'study-note': studyNote.entries.map((entry) => String(entry.handler.command)) };
|
|
2103
|
+
const host = args.includes('--probe')
|
|
2104
|
+
? await probeHookDiscovery({ executable: await findExecutable('codex') || 'codex', cwd, commands, configPath: target })
|
|
2105
|
+
: { state: 'not_measured', active_session_loaded: 'not_measured', execution_observed: false, truth_status: 'partial' };
|
|
2106
|
+
const profiles = {};
|
|
2107
|
+
for (const [profile, health] of [['lite', lite], ['study-note', studyNote]]) {
|
|
2108
|
+
profiles[profile] = { configured: health.state, issues: health.issues, loaded: 'unknown',
|
|
2109
|
+
execution: await inspectHookObservation(cwd, profile, YAM_HOOK_ENTRYPOINT) };
|
|
2110
|
+
}
|
|
2111
|
+
if (args.includes('--json')) {
|
|
2112
|
+
console.log(JSON.stringify({ schema: 'yam.hook-readiness.v1', scope: parsed.mode, profiles, host, truth_status: 'partial' }, null, 2));
|
|
2113
|
+
if (lite.state === 'broken' || studyNote.state === 'broken')
|
|
2114
|
+
process.exitCode = 1;
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2058
2117
|
printHookProfileStatus('lite', lite);
|
|
2059
2118
|
printHookProfileStatus('study-note', studyNote);
|
|
2060
2119
|
console.log(`scope: ${parsed.mode}`);
|
|
2061
2120
|
console.log(`file: ${target}`);
|
|
2121
|
+
console.log('active Codex session loaded: unknown (configuration is not runtime evidence)');
|
|
2122
|
+
console.log(`host discovery: ${host.state}`);
|
|
2123
|
+
for (const profile of ['lite', 'study-note'])
|
|
2124
|
+
console.log(`yam-${profile} execution: ${profiles[profile].execution.state} (local entrypoint evidence, not host attestation)`);
|
|
2062
2125
|
if (lite.state === 'broken' || studyNote.state === 'broken')
|
|
2063
2126
|
process.exitCode = 1;
|
|
2064
2127
|
}
|
|
@@ -2066,7 +2129,7 @@ function hookConfigHasYamLite(config = {}) {
|
|
|
2066
2129
|
return hookConfigHasProfile(config, 'lite');
|
|
2067
2130
|
}
|
|
2068
2131
|
function hookConfigHasProfile(config = {}, profile = 'lite') {
|
|
2069
|
-
return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
2132
|
+
return Object.values(hookEventMap(config)).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
2070
2133
|
return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
|
|
2071
2134
|
}));
|
|
2072
2135
|
}
|
|
@@ -2078,7 +2141,7 @@ function hookEventsForProfile(profile = 'lite') {
|
|
|
2078
2141
|
}
|
|
2079
2142
|
function hookProfileEntries(config = {}, profile = 'lite') {
|
|
2080
2143
|
const matches = [];
|
|
2081
|
-
for (const [event, entries] of Object.entries(config)) {
|
|
2144
|
+
for (const [event, entries] of Object.entries(hookEventMap(config))) {
|
|
2082
2145
|
if (!Array.isArray(entries))
|
|
2083
2146
|
continue;
|
|
2084
2147
|
for (const entry of entries) {
|
|
@@ -2142,6 +2205,8 @@ function splitHookCommand(command = '') {
|
|
|
2142
2205
|
}
|
|
2143
2206
|
function parseYamHookCommand(command = '', profile = 'lite') {
|
|
2144
2207
|
const words = splitHookCommand(command);
|
|
2208
|
+
if (words.at(-1) === '--observe')
|
|
2209
|
+
words.pop();
|
|
2145
2210
|
if (words.length === 5 && words[2] === 'hook' && words[3] === 'run' && words[4] === profile) {
|
|
2146
2211
|
const nodeName = path.basename(words[0]).toLowerCase();
|
|
2147
2212
|
if (/^node(?:\.exe)?$/.test(nodeName) && path.basename(words[1]).toLowerCase() === 'yam.js') {
|
|
@@ -2185,6 +2250,8 @@ async function inspectHookProfile(config = {}, profile = 'lite') {
|
|
|
2185
2250
|
if (!entries.length)
|
|
2186
2251
|
return { state: 'disabled', issues: [], entries };
|
|
2187
2252
|
const issues = [];
|
|
2253
|
+
if (Object.keys(config).some((event) => CODEX_HOOK_EVENTS.has(event)))
|
|
2254
|
+
issues.push('legacy hook config requires the top-level hooks wrapper; rerun yam hook enable to migrate with backup');
|
|
2188
2255
|
for (const event of hookEventsForProfile(profile)) {
|
|
2189
2256
|
const count = entries.filter((entry) => entry.event === event).length;
|
|
2190
2257
|
if (count === 0)
|
|
@@ -2213,6 +2280,9 @@ async function readHookConfig(target) {
|
|
|
2213
2280
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
2214
2281
|
return { config: {}, exists: true, error: 'top-level value must be a JSON object' };
|
|
2215
2282
|
}
|
|
2283
|
+
if (value.hooks != null && (typeof value.hooks !== 'object' || Array.isArray(value.hooks))) {
|
|
2284
|
+
return { config: {}, exists: true, error: 'hooks must be an event object' };
|
|
2285
|
+
}
|
|
2216
2286
|
return { config: value, exists: true, error: '' };
|
|
2217
2287
|
}
|
|
2218
2288
|
catch (error) {
|
|
@@ -2233,8 +2303,41 @@ async function writeHookConfig(target, config) {
|
|
|
2233
2303
|
throw error;
|
|
2234
2304
|
}
|
|
2235
2305
|
}
|
|
2306
|
+
async function hookMigrate(args = []) {
|
|
2307
|
+
const parsed = parseHookArgs(args);
|
|
2308
|
+
const target = hookPathFor(parsed);
|
|
2309
|
+
const loaded = await readHookConfig(target);
|
|
2310
|
+
if (loaded.error) {
|
|
2311
|
+
console.error('hook migration blocked: unreadable configuration');
|
|
2312
|
+
process.exitCode = 1;
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (!loaded.exists || !Object.keys(loaded.config).some((key) => CODEX_HOOK_EVENTS.has(key))) {
|
|
2316
|
+
console.log('hook migration: no legacy event layout found');
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const before = await fsp.lstat(target);
|
|
2320
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
|
|
2321
|
+
throw new Error('hook migration blocked: unsafe config file');
|
|
2322
|
+
const original = await fsp.readFile(target, 'utf8');
|
|
2323
|
+
if (JSON.stringify(JSON.parse(original)) !== JSON.stringify(loaded.config))
|
|
2324
|
+
throw new Error('hook configuration changed; retry migration');
|
|
2325
|
+
const next = canonicalHookConfig(loaded.config);
|
|
2326
|
+
const backup = `${target}.yam-backup-${timestampId()}`;
|
|
2327
|
+
await fsp.copyFile(target, backup, fs.constants.COPYFILE_EXCL);
|
|
2328
|
+
const after = await fsp.lstat(target);
|
|
2329
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.mtimeMs !== before.mtimeMs
|
|
2330
|
+
|| await fsp.readFile(target, 'utf8') !== original)
|
|
2331
|
+
throw new Error('hook configuration changed; backup preserved, retry migration');
|
|
2332
|
+
await writeHookConfig(target, next);
|
|
2333
|
+
console.log(`hook migration: wrapped event groups; handlers and metadata preserved\nbackup: ${backup}\nReview changed definitions in Codex /hooks before enabling execution.`);
|
|
2334
|
+
}
|
|
2236
2335
|
async function hookEnable(args = []) {
|
|
2237
2336
|
const parsed = parseHookArgs(args);
|
|
2337
|
+
const node = await selectHookNode();
|
|
2338
|
+
const observe = args.includes('--observe') ? ' --observe' : '';
|
|
2339
|
+
YAM_LITE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite${observe}`;
|
|
2340
|
+
YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(node.executable)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note${observe}`;
|
|
2238
2341
|
if (!['lite', 'study-note'].includes(parsed.profile)) {
|
|
2239
2342
|
console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
|
|
2240
2343
|
process.exitCode = 1;
|
|
@@ -2263,6 +2366,7 @@ async function hookEnable(args = []) {
|
|
|
2263
2366
|
}
|
|
2264
2367
|
await writeHookConfig(target, next);
|
|
2265
2368
|
console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
|
|
2369
|
+
console.log(`node launcher: ${node.selection}; alias identity matched the current runtime (no PATH candidate executed)`);
|
|
2266
2370
|
if (needsMigration)
|
|
2267
2371
|
console.log(`migrated: ${existing.length} existing yam-${parsed.profile} handler(s) to the current command and event coverage`);
|
|
2268
2372
|
console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
|
|
@@ -2283,7 +2387,7 @@ async function hookDisable(args = []) {
|
|
|
2283
2387
|
return;
|
|
2284
2388
|
}
|
|
2285
2389
|
const next = stripYamHooks(current, parsed.profile);
|
|
2286
|
-
if (Object.keys(next).length === 0) {
|
|
2390
|
+
if (Object.keys(next).length === 1 && Object.keys(next.hooks).length === 0) {
|
|
2287
2391
|
await rmrf(target);
|
|
2288
2392
|
}
|
|
2289
2393
|
else {
|
|
@@ -2313,13 +2417,15 @@ async function hookRun(args = []) {
|
|
|
2313
2417
|
}
|
|
2314
2418
|
const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
|
|
2315
2419
|
const cwd = String(input?.cwd || process.cwd());
|
|
2420
|
+
if (args.includes('--observe'))
|
|
2421
|
+
await recordHookObservation(cwd, profile, String(event), YAM_HOOK_ENTRYPOINT).catch(() => { });
|
|
2316
2422
|
if (profile === 'study-note' && event === 'Stop') {
|
|
2317
2423
|
console.log(JSON.stringify(await buildStudyNoteStopOutput(input, cwd)));
|
|
2318
2424
|
return;
|
|
2319
2425
|
}
|
|
2320
2426
|
const prompt = extractPrompt(input);
|
|
2321
2427
|
const additionalContext = profile === 'study-note'
|
|
2322
|
-
? await
|
|
2428
|
+
? await buildStudyNoteTurnContext(input, cwd)
|
|
2323
2429
|
: await buildYamLiteContext({ cwd, prompt });
|
|
2324
2430
|
const output = {
|
|
2325
2431
|
continue: true,
|
|
@@ -2348,6 +2454,7 @@ function extractPrompt(input = {}) {
|
|
|
2348
2454
|
async function buildYamLiteContext({ cwd, prompt }) {
|
|
2349
2455
|
const lines = [
|
|
2350
2456
|
'yam-lite guide active: keep project direction visible, preserve momentum, and deepen when scope, risk, or user intent calls for it.',
|
|
2457
|
+
'Git workflow: keep the current branch; create/switch branches, worktrees or PRs only on explicit user request or applicable project rule. Commit/push, release bumps, branch-name prefixes and protection alone do not authorize them. If blocked, report and ask; do not silently change workflow or protections.',
|
|
2351
2458
|
'Default: do a basic direction-fit and honest-verification check; avoid broad reading for small work and never claim proof without evidence.'
|
|
2352
2459
|
];
|
|
2353
2460
|
const pack = await findProjectPack(cwd).catch(() => null);
|
|
@@ -2367,37 +2474,10 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
2367
2474
|
lines.push(routeHint);
|
|
2368
2475
|
return lines.join('\n');
|
|
2369
2476
|
}
|
|
2370
|
-
async function buildStudyNoteHookContext({ cwd }) {
|
|
2371
|
-
const dir = path.resolve(cwd || process.cwd());
|
|
2372
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2373
|
-
const changedFiles = changedFileDetection.files;
|
|
2374
|
-
const lines = [
|
|
2375
|
-
'yam Study Note guard active: if this turn changes code, config, release metadata, docs, or project artifacts, include a Study Note followed immediately by Next step in the final response.'
|
|
2376
|
-
];
|
|
2377
|
-
if (!changedFileDetection.available) {
|
|
2378
|
-
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2379
|
-
lines.push('Determine manually whether artifacts changed and include Study Note followed by Next step when they did; the Stop hook will keep this uncertainty visible.');
|
|
2380
|
-
return lines.join('\n');
|
|
2381
|
-
}
|
|
2382
|
-
if (!changedFiles.length) {
|
|
2383
|
-
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add Study Note followed immediately by Next step before final.');
|
|
2384
|
-
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2385
|
-
return lines.join('\n');
|
|
2386
|
-
}
|
|
2387
|
-
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2388
|
-
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2389
|
-
lines.push('Next step minimum: quick whole-process scan, current situation, forward outlook, critical opinion, improvement recommendation, ordered fix-first then planned actions, evidence, ownership, blocker status, safe retry, side effects, and truth status.');
|
|
2390
|
-
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2391
|
-
if (hygiene.length) {
|
|
2392
|
-
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2393
|
-
lines.push('Report whether the change avoided dumping unrelated logic into page.tsx, one-off CSS into global.css, or structured product data into broad DB jsonb.');
|
|
2394
|
-
}
|
|
2395
|
-
lines.push('This prompt reminder does not generate or edit either section; the paired Stop hook requests one correction pass if the final response is incomplete or ordered incorrectly.');
|
|
2396
|
-
return lines.join('\n');
|
|
2397
|
-
}
|
|
2398
2477
|
async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
2399
2478
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
2400
|
-
const
|
|
2479
|
+
const scope = studyNoteTurnScope(input, cwd);
|
|
2480
|
+
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message', scope);
|
|
2401
2481
|
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2402
2482
|
return {
|
|
2403
2483
|
continue: true,
|
|
@@ -2406,7 +2486,11 @@ async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
|
2406
2486
|
}
|
|
2407
2487
|
if (!result.blockers.length)
|
|
2408
2488
|
return { continue: true };
|
|
2409
|
-
const
|
|
2489
|
+
const failed = result.checks.filter(check => check.status === 'fail').map(check => check.id);
|
|
2490
|
+
const summary = failed.includes('study_note_present') ? 'study_note_present' : failed.join(', ');
|
|
2491
|
+
if (scope.mode !== 'strict') {
|
|
2492
|
+
return { continue: true, systemMessage: `yam Study Note advisory — missing fields: ${summary}. Keep ordinary work concise; no correction gate applied.` };
|
|
2493
|
+
}
|
|
2410
2494
|
if (Boolean(input?.stop_hook_active || input?.stopHookActive)) {
|
|
2411
2495
|
return {
|
|
2412
2496
|
continue: true,
|
|
@@ -2416,12 +2500,17 @@ async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
|
2416
2500
|
return {
|
|
2417
2501
|
decision: 'block',
|
|
2418
2502
|
reason: [
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
'Revise the final response to include: touched artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, limits, and any relevant architecture hygiene.'
|
|
2503
|
+
`yam Study Note strict — missing fields: ${summary}.`,
|
|
2504
|
+
'Revise only the missing fields in Study Note / Next step; this is report validation, not a security scan.'
|
|
2422
2505
|
].join(' ')
|
|
2423
2506
|
};
|
|
2424
2507
|
}
|
|
2508
|
+
async function buildStudyNoteTurnContext(input, cwd) {
|
|
2509
|
+
const scope = studyNoteTurnScope(input, cwd, true);
|
|
2510
|
+
if (!scope.available)
|
|
2511
|
+
return 'yam Study Note: turn scope is unavailable; do not treat this as a clean project. Inspect changed artifacts manually; no strict block will be applied without a baseline.';
|
|
2512
|
+
return `yam Study Note turn baseline saved (hash-only). Mode: ${scope.mode}. Only changes since this prompt count; known generated logs, mission/security receipts and screenshots are excluded unless explicitly included. Include a concise Study Note followed by Next step when product artifacts change. ${scope.mode === 'strict' ? 'Risk work uses the detailed report contract and one correction pass.' : 'Ordinary work is advisory: a short explanation, verification/limits and remaining work (or none) suffice.'}`;
|
|
2513
|
+
}
|
|
2425
2514
|
function yamLiteRouteHint(prompt = '') {
|
|
2426
2515
|
const text = String(prompt || '').toLowerCase();
|
|
2427
2516
|
if (!text)
|
|
@@ -6079,7 +6168,7 @@ async function memory(args = []) {
|
|
|
6079
6168
|
}
|
|
6080
6169
|
if (subcommand === 'summary' || subcommand === 'summarize') {
|
|
6081
6170
|
const parsed = parseMemoryArgs(args.slice(1));
|
|
6082
|
-
return memorySummary(parsed.dir);
|
|
6171
|
+
return memorySummary(parsed.dir, { json: args.includes('--json') });
|
|
6083
6172
|
}
|
|
6084
6173
|
if (subcommand === 'resolve')
|
|
6085
6174
|
return memoryResolve(args.slice(1));
|
|
@@ -6093,7 +6182,7 @@ Usage:
|
|
|
6093
6182
|
yam memory init [dir]
|
|
6094
6183
|
yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
|
|
6095
6184
|
yam memory list [dir] [--json]
|
|
6096
|
-
yam memory summary [dir]
|
|
6185
|
+
yam memory summary [dir] [--json]
|
|
6097
6186
|
yam memory resolve [dir] <id> [--note <text>]
|
|
6098
6187
|
|
|
6099
6188
|
Kinds:
|
|
@@ -6163,7 +6252,17 @@ async function memoryAdd(args = []) {
|
|
|
6163
6252
|
console.log(`memory added: ${id}`);
|
|
6164
6253
|
}
|
|
6165
6254
|
async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
6166
|
-
const
|
|
6255
|
+
const report = await readMemoryRecords(targetDir);
|
|
6256
|
+
const records = report.records;
|
|
6257
|
+
if (!report.complete) {
|
|
6258
|
+
if (report.state !== 'missing')
|
|
6259
|
+
process.exitCode = 1;
|
|
6260
|
+
if (json)
|
|
6261
|
+
console.log(JSON.stringify(report, null, 2));
|
|
6262
|
+
else
|
|
6263
|
+
console.error(`memory read ${report.state}: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
|
|
6264
|
+
return;
|
|
6265
|
+
}
|
|
6167
6266
|
if (json) {
|
|
6168
6267
|
console.log(JSON.stringify(records, null, 2));
|
|
6169
6268
|
return;
|
|
@@ -6178,9 +6277,18 @@ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
|
|
|
6178
6277
|
console.log(` action: ${record.action}`);
|
|
6179
6278
|
}
|
|
6180
6279
|
}
|
|
6181
|
-
async function memorySummary(targetDir = process.cwd()) {
|
|
6280
|
+
async function memorySummary(targetDir = process.cwd(), { json = false } = {}) {
|
|
6182
6281
|
const dir = memoryDir(targetDir);
|
|
6183
|
-
const
|
|
6282
|
+
const report = await readMemoryRecords(targetDir);
|
|
6283
|
+
if (!report.complete) {
|
|
6284
|
+
process.exitCode = 1;
|
|
6285
|
+
if (json)
|
|
6286
|
+
console.log(JSON.stringify({ ...report, summary_written: false }, null, 2));
|
|
6287
|
+
else
|
|
6288
|
+
console.error(`memory summary blocked (${report.state}); existing summary preserved: ${JSON.stringify(report.diagnostics)}${report.diagnostics_truncated ? ' (diagnostics truncated)' : ''}`);
|
|
6289
|
+
return;
|
|
6290
|
+
}
|
|
6291
|
+
const records = report.records;
|
|
6184
6292
|
await fsp.mkdir(dir, { recursive: true });
|
|
6185
6293
|
const active = records.filter((record) => record.status !== 'resolved');
|
|
6186
6294
|
const resolved = records.filter((record) => record.status === 'resolved');
|
|
@@ -6212,7 +6320,10 @@ async function memorySummary(targetDir = process.cwd()) {
|
|
|
6212
6320
|
}
|
|
6213
6321
|
const target = path.join(dir, 'summary.md');
|
|
6214
6322
|
await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
|
|
6215
|
-
|
|
6323
|
+
if (json)
|
|
6324
|
+
console.log(JSON.stringify({ ...report, summary_written: true, path: target }, null, 2));
|
|
6325
|
+
else
|
|
6326
|
+
console.log(`memory summary written: ${target}`);
|
|
6216
6327
|
}
|
|
6217
6328
|
async function memoryResolve(args = []) {
|
|
6218
6329
|
const parsed = parseMemoryArgs(args);
|
|
@@ -6267,23 +6378,7 @@ function memoryDir(targetDir = process.cwd()) {
|
|
|
6267
6378
|
}
|
|
6268
6379
|
async function readMemoryRecords(targetDir = process.cwd()) {
|
|
6269
6380
|
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 || '')));
|
|
6381
|
+
return readMemoryDirectory(recordsDir);
|
|
6287
6382
|
}
|
|
6288
6383
|
function timestampId() {
|
|
6289
6384
|
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.(\d{3})Z$/, '$1z');
|