yam-flow 2.8.2 → 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 +15 -0
- package/README.md +7 -2
- package/dist/bin/yam.js +30 -38
- package/dist/lib/study-note-scope.d.ts +8 -0
- package/dist/lib/study-note-scope.js +165 -0
- package/package.json +3 -2
- 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 +29 -39
- 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,21 @@
|
|
|
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
|
+
|
|
5
20
|
## v2.8.2 - 2026-10-02
|
|
6
21
|
|
|
7
22
|
### Improved
|
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.
|
|
@@ -102,7 +103,11 @@ yam hook enable study-note --global
|
|
|
102
103
|
yam hook status --global
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
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.
|
|
106
111
|
|
|
107
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.
|
|
108
113
|
|
package/dist/bin/yam.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { studyNoteTurnScope } from '../lib/study-note-scope.js';
|
|
3
4
|
import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
|
|
4
5
|
import { readMemoryDirectory } from '../lib/memory-records.js';
|
|
5
6
|
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
@@ -934,8 +935,8 @@ async function studyNoteCheck(args = []) {
|
|
|
934
935
|
if (result.blockers.length)
|
|
935
936
|
process.exitCode = 1;
|
|
936
937
|
}
|
|
937
|
-
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
|
|
938
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
938
|
+
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided', scope = null) {
|
|
939
|
+
const changedFileDetection = scope || gitChangedFileSnapshot(dir);
|
|
939
940
|
const changedFiles = changedFileDetection.files;
|
|
940
941
|
const changeScopeAvailable = changedFileDetection.available;
|
|
941
942
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
@@ -966,6 +967,14 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
966
967
|
for (const requirement of hygieneRequirements) {
|
|
967
968
|
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
968
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
|
+
}
|
|
969
978
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
970
979
|
const result = {
|
|
971
980
|
schema: 'yam.study-note-guard.v1',
|
|
@@ -1054,7 +1063,7 @@ function hasNextStepMarker(text = '') {
|
|
|
1054
1063
|
}
|
|
1055
1064
|
function reportSectionMarkers(text = '') {
|
|
1056
1065
|
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1057
|
-
const trimmed = line.trim();
|
|
1066
|
+
const trimmed = line.trim().replace(/^\*\*(.*?)\*\*\s*:?[ \t]*$/, '$1');
|
|
1058
1067
|
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1059
1068
|
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1060
1069
|
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
@@ -1178,7 +1187,7 @@ function studyNoteHygieneRequirements(files = []) {
|
|
|
1178
1187
|
if (files.some((file) => /(^|\/)global\.css$/i.test(file))) {
|
|
1179
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' });
|
|
1180
1189
|
}
|
|
1181
|
-
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))) {
|
|
1182
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' });
|
|
1183
1192
|
}
|
|
1184
1193
|
return requirements;
|
|
@@ -2416,7 +2425,7 @@ async function hookRun(args = []) {
|
|
|
2416
2425
|
}
|
|
2417
2426
|
const prompt = extractPrompt(input);
|
|
2418
2427
|
const additionalContext = profile === 'study-note'
|
|
2419
|
-
? await
|
|
2428
|
+
? await buildStudyNoteTurnContext(input, cwd)
|
|
2420
2429
|
: await buildYamLiteContext({ cwd, prompt });
|
|
2421
2430
|
const output = {
|
|
2422
2431
|
continue: true,
|
|
@@ -2445,6 +2454,7 @@ function extractPrompt(input = {}) {
|
|
|
2445
2454
|
async function buildYamLiteContext({ cwd, prompt }) {
|
|
2446
2455
|
const lines = [
|
|
2447
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.',
|
|
2448
2458
|
'Default: do a basic direction-fit and honest-verification check; avoid broad reading for small work and never claim proof without evidence.'
|
|
2449
2459
|
];
|
|
2450
2460
|
const pack = await findProjectPack(cwd).catch(() => null);
|
|
@@ -2464,37 +2474,10 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
2464
2474
|
lines.push(routeHint);
|
|
2465
2475
|
return lines.join('\n');
|
|
2466
2476
|
}
|
|
2467
|
-
async function buildStudyNoteHookContext({ cwd }) {
|
|
2468
|
-
const dir = path.resolve(cwd || process.cwd());
|
|
2469
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2470
|
-
const changedFiles = changedFileDetection.files;
|
|
2471
|
-
const lines = [
|
|
2472
|
-
'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.'
|
|
2473
|
-
];
|
|
2474
|
-
if (!changedFileDetection.available) {
|
|
2475
|
-
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2476
|
-
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.');
|
|
2477
|
-
return lines.join('\n');
|
|
2478
|
-
}
|
|
2479
|
-
if (!changedFiles.length) {
|
|
2480
|
-
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.');
|
|
2481
|
-
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2482
|
-
return lines.join('\n');
|
|
2483
|
-
}
|
|
2484
|
-
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2485
|
-
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2486
|
-
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.');
|
|
2487
|
-
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2488
|
-
if (hygiene.length) {
|
|
2489
|
-
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2490
|
-
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.');
|
|
2491
|
-
}
|
|
2492
|
-
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.');
|
|
2493
|
-
return lines.join('\n');
|
|
2494
|
-
}
|
|
2495
2477
|
async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
2496
2478
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
2497
|
-
const
|
|
2479
|
+
const scope = studyNoteTurnScope(input, cwd);
|
|
2480
|
+
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message', scope);
|
|
2498
2481
|
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2499
2482
|
return {
|
|
2500
2483
|
continue: true,
|
|
@@ -2503,7 +2486,11 @@ async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
|
2503
2486
|
}
|
|
2504
2487
|
if (!result.blockers.length)
|
|
2505
2488
|
return { continue: true };
|
|
2506
|
-
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
|
+
}
|
|
2507
2494
|
if (Boolean(input?.stop_hook_active || input?.stopHookActive)) {
|
|
2508
2495
|
return {
|
|
2509
2496
|
continue: true,
|
|
@@ -2513,12 +2500,17 @@ async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
|
2513
2500
|
return {
|
|
2514
2501
|
decision: 'block',
|
|
2515
2502
|
reason: [
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
'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.'
|
|
2519
2505
|
].join(' ')
|
|
2520
2506
|
};
|
|
2521
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
|
+
}
|
|
2522
2514
|
function yamLiteRouteHint(prompt = '') {
|
|
2523
2515
|
const text = String(prompt || '').toLowerCase();
|
|
2524
2516
|
if (!text)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
7
|
+
const MAX_FILES = 4000;
|
|
8
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
function git(dir, args) {
|
|
10
|
+
const result = spawnSync('git', args, { cwd: dir, encoding: 'utf8', timeout: 750, maxBuffer: MAX_BYTES,
|
|
11
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } });
|
|
12
|
+
if (result.status !== 0)
|
|
13
|
+
throw new Error('git_scope_unavailable');
|
|
14
|
+
return result.stdout;
|
|
15
|
+
}
|
|
16
|
+
function snapshot(dir) {
|
|
17
|
+
const root = fs.realpathSync(git(dir, ['rev-parse', '--show-toplevel']).trim());
|
|
18
|
+
const config = policy(root);
|
|
19
|
+
const files = Object.create(null);
|
|
20
|
+
let bytesRead = 0;
|
|
21
|
+
const tracked = git(root, ['ls-files', '--stage', '-z']).split('\0').filter(Boolean);
|
|
22
|
+
for (const row of tracked) {
|
|
23
|
+
const tab = row.indexOf('\t');
|
|
24
|
+
if (selected(row.slice(tab + 1), config))
|
|
25
|
+
files[row.slice(tab + 1)] = hash(row.slice(0, tab));
|
|
26
|
+
}
|
|
27
|
+
const rows = git(root, ['status', '--short', '-z', '--untracked-files=all']).split('\0');
|
|
28
|
+
for (let i = 0; i < rows.length; i++) {
|
|
29
|
+
const row = rows[i];
|
|
30
|
+
if (!row)
|
|
31
|
+
continue;
|
|
32
|
+
const name = row.slice(3);
|
|
33
|
+
if (/[RC]/.test(row.slice(0, 2)))
|
|
34
|
+
i++;
|
|
35
|
+
if (!selected(name, config))
|
|
36
|
+
continue;
|
|
37
|
+
const target = path.resolve(root, name);
|
|
38
|
+
if (!target.startsWith(root + path.sep))
|
|
39
|
+
throw new Error('unsafe_path');
|
|
40
|
+
// Do not follow symlinks, including symlinked parent directories.
|
|
41
|
+
let parent = path.dirname(target);
|
|
42
|
+
while (parent !== root) {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.lstatSync(parent).isSymbolicLink())
|
|
45
|
+
throw new Error('unsafe_path');
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code !== 'ENOENT')
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
parent = path.dirname(parent);
|
|
52
|
+
}
|
|
53
|
+
let content = 'missing';
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.lstatSync(target);
|
|
56
|
+
if (stat.isSymbolicLink())
|
|
57
|
+
content = 'link:' + hash(fs.readlinkSync(target));
|
|
58
|
+
else if (stat.isFile() && (bytesRead += stat.size) <= MAX_BYTES)
|
|
59
|
+
content = hash(fs.readFileSync(target));
|
|
60
|
+
else
|
|
61
|
+
throw new Error('scope_limit');
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== 'ENOENT')
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
files[name] = hash(JSON.stringify([row.slice(0, 2), files[name] || '', content]));
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(files).length > MAX_FILES)
|
|
70
|
+
throw new Error('scope_limit');
|
|
71
|
+
return { root, files };
|
|
72
|
+
}
|
|
73
|
+
function policy(root) {
|
|
74
|
+
const target = path.join(root, '.yam', 'study-note.json');
|
|
75
|
+
let value = {};
|
|
76
|
+
if (fs.existsSync(target)) {
|
|
77
|
+
if (fs.lstatSync(path.dirname(target)).isSymbolicLink() || fs.lstatSync(target).isSymbolicLink()
|
|
78
|
+
|| fs.statSync(target).size > 16384)
|
|
79
|
+
throw new Error('invalid_policy');
|
|
80
|
+
value = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
81
|
+
if (!value || !['advisory', 'strict', undefined].includes(value.mode))
|
|
82
|
+
throw new Error('invalid_policy');
|
|
83
|
+
}
|
|
84
|
+
const list = (key) => {
|
|
85
|
+
const result = value[key] || [];
|
|
86
|
+
if (!Array.isArray(result) || result.length > 100 || result.some((x) => typeof x !== 'string'
|
|
87
|
+
|| !x || x.startsWith('/') || x.includes('..') || /[\\\0]/.test(x) || x.replace(/\/\*\*$/, '').includes('*')))
|
|
88
|
+
throw new Error('invalid_policy');
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
return { include: list('include'), exclude: list('exclude'), strict: value.mode === 'strict' };
|
|
92
|
+
}
|
|
93
|
+
const matches = (name, pattern) => pattern.endsWith('/**')
|
|
94
|
+
? name.startsWith(pattern.slice(0, -2)) : name === pattern;
|
|
95
|
+
function selected(name, config) {
|
|
96
|
+
if (config.include.some(p => matches(name, p)))
|
|
97
|
+
return true;
|
|
98
|
+
if (config.exclude.some(p => matches(name, p)))
|
|
99
|
+
return false;
|
|
100
|
+
return !/^(dist\/|\.yam\/(mission|security|logs|screenshots)\/)/.test(name)
|
|
101
|
+
&& !/\.tgz$/.test(name);
|
|
102
|
+
}
|
|
103
|
+
function statePath(root, session) {
|
|
104
|
+
const dir = path.join(os.tmpdir(), `yam-study-note-${process.getuid?.() ?? 'user'}`);
|
|
105
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
106
|
+
const stat = fs.lstatSync(dir);
|
|
107
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid())
|
|
108
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077)))
|
|
109
|
+
throw new Error('unsafe_state');
|
|
110
|
+
return path.join(dir, hash(root + '\0' + session) + '.json');
|
|
111
|
+
}
|
|
112
|
+
/** Hash-only, bounded scope evidence; not a security attestation. */
|
|
113
|
+
export function studyNoteTurnScope(input, cwd, begin = false) {
|
|
114
|
+
try {
|
|
115
|
+
const session = String(input.session_id || input.sessionId || input.thread_id || '');
|
|
116
|
+
const turn = String(input.turn_id || input.turnId || '');
|
|
117
|
+
if (!session || session.length > 512 || turn.length > 512)
|
|
118
|
+
throw new Error('session_unavailable');
|
|
119
|
+
const current = snapshot(cwd);
|
|
120
|
+
const config = policy(current.root);
|
|
121
|
+
const target = statePath(current.root, session);
|
|
122
|
+
if (begin) {
|
|
123
|
+
const prompt = String(input.prompt || input.user_prompt || '');
|
|
124
|
+
// Deep/Mission alone do not make a read-only task strict. Risk intent or explicit strict does.
|
|
125
|
+
const strict = config.strict || /(?:study-note\s+strict|strict\s+study.note|commit|push|deploy|publish|security|migration|database|\bDB\b|커밋|푸시|배포|보안|마이그레이션|데이터베이스)/i.test(prompt);
|
|
126
|
+
const state = { schema: 'yam.study-note-baseline.v1', created: Date.now(), turn, strict, ...current };
|
|
127
|
+
try {
|
|
128
|
+
const previous = fs.lstatSync(target);
|
|
129
|
+
if (!previous.isFile() || previous.isSymbolicLink() || previous.nlink !== 1
|
|
130
|
+
|| (process.getuid && previous.uid !== process.getuid()))
|
|
131
|
+
throw new Error('unsafe_state');
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code !== 'ENOENT')
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
const fd = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
138
|
+
try {
|
|
139
|
+
fs.writeFileSync(fd, JSON.stringify(state));
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
fs.closeSync(fd);
|
|
143
|
+
}
|
|
144
|
+
return { available: true, files: [], source: 'turn_baseline', mode: strict ? 'strict' : 'advisory', reason: '' };
|
|
145
|
+
}
|
|
146
|
+
const stat = fs.lstatSync(target);
|
|
147
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_BYTES)
|
|
148
|
+
throw new Error('invalid_baseline');
|
|
149
|
+
const before = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
150
|
+
if (before.schema !== 'yam.study-note-baseline.v1' || before.root !== current.root
|
|
151
|
+
|| !before.files || typeof before.files !== 'object' || Array.isArray(before.files)
|
|
152
|
+
|| !Number.isFinite(before.created) || typeof before.strict !== 'boolean'
|
|
153
|
+
|| Object.keys(before.files).length > MAX_FILES
|
|
154
|
+
|| Object.values(before.files).some(value => typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value))
|
|
155
|
+
|| Date.now() - before.created > 24 * 60 * 60 * 1000 || before.created > Date.now()
|
|
156
|
+
|| (turn && before.turn !== turn))
|
|
157
|
+
throw new Error('stale_baseline');
|
|
158
|
+
const files = [...new Set([...Object.keys(before.files), ...Object.keys(current.files)])]
|
|
159
|
+
.filter(name => before.files[name] !== current.files[name] && selected(name, config));
|
|
160
|
+
return { available: true, files, source: 'turn_baseline', mode: config.strict || before.strict ? 'strict' : 'advisory', reason: '' };
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { available: false, files: [], source: 'turn_baseline', mode: 'advisory', reason: 'baseline_unavailable; inspect changed artifacts manually' };
|
|
164
|
+
}
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yam-flow",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
4
4
|
"description": "Progressive proof-first agent flow: start fast, deepen deliberately, stay honest by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "0kim0bos",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"forbidden-names:check": "node ./scripts/check-forbidden-names.mjs",
|
|
70
70
|
"workflow-pins:check": "node ./scripts/check-workflow-pins.mjs",
|
|
71
71
|
"study-note:check": "node ./dist/bin/yam.js study-note check --json",
|
|
72
|
+
"study-note-turn-smoke": "node ./scripts/study-note-turn-smoke.mjs",
|
|
72
73
|
"package-boundary:check": "node ./scripts/check-package-boundary.mjs",
|
|
73
74
|
"package-secrets:check": "node ./scripts/check-package-secrets.mjs",
|
|
74
75
|
"package-secrets-smoke": "node ./scripts/package-secrets-smoke.mjs",
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
"trust-adoptions-smoke": "npm run build && node ./scripts/trust-adoptions-smoke.mjs",
|
|
87
88
|
"design-production-smoke": "npm run build && node ./scripts/design-production-smoke.mjs",
|
|
88
89
|
"phase-adoptions-smoke": "npm run build && node ./scripts/phase-adoptions-smoke.mjs",
|
|
89
|
-
"hook-latency-smoke": "node ./scripts/hook-latency-smoke.mjs",
|
|
90
|
+
"hook-latency-smoke": "node ./scripts/hook-latency-smoke.mjs && node ./scripts/study-note-turn-smoke.mjs",
|
|
90
91
|
"install-transaction-smoke": "npm run build && node ./scripts/install-transaction-smoke.mjs",
|
|
91
92
|
"next-step-smoke": "node ./scripts/next-step-smoke.mjs",
|
|
92
93
|
"scout-receipt-smoke": "node ./scripts/scout-receipt-smoke.mjs",
|
|
@@ -40,6 +40,7 @@ Use it only for known wrong decisions, repeated mistakes, direction changes, and
|
|
|
40
40
|
|
|
41
41
|
Rules:
|
|
42
42
|
|
|
43
|
+
- Work on the current branch by default. Creating/switching branches, creating worktrees and creating PRs each need an explicit user request or applicable project rule. Commit/push, release bumps and a `codex/` naming convention do not authorize these actions. Branch protection specifies acceptance conditions, not a mandate to create a branch; when blocked, report and ask instead of changing workflow or protections. Preserve existing work; if detached HEAD prevents a requested commit, clarify the target before changing it.
|
|
43
44
|
- Do not regenerate a broad plan when `yam.project.md` already answers the direction question.
|
|
44
45
|
- Do not reread architecture docs unless the task touches architecture.
|
|
45
46
|
- Do not reread full design docs for tiny UI work when the pack has the relevant style direction.
|
|
@@ -21,7 +21,7 @@ Always keep final reports short. Include these when they apply:
|
|
|
21
21
|
- Publish blocker evidence when npm/release work fails.
|
|
22
22
|
- Structured diagnostic next action, when a check found a concrete next step.
|
|
23
23
|
- Study Note whenever code, config, release metadata, documentation, or project artifacts changed, even if no yam skill was explicitly invoked. This item is required for changed artifacts, not optional.
|
|
24
|
-
- Next step immediately after Study Note for changed-artifact reports.
|
|
24
|
+
- Next step immediately after Study Note for changed-artifact reports. Ordinary advisory work may use one concrete action or no remaining work; strict reports use the whole-process scan and ordered fix-first/planned contract.
|
|
25
25
|
- Remaining tasks.
|
|
26
26
|
- Fix-first items before planned tasks.
|
|
27
27
|
- Recommended direction and why that next step matters when the handoff would otherwise be ambiguous.
|
|
@@ -90,6 +90,8 @@ Include:
|
|
|
90
90
|
- One ordered sequence with all fix-first items before planned work.
|
|
91
91
|
- Why each action matters, owner route and scope, blockers and safe retry, evidence level and stamp, expected side effects, and truth status.
|
|
92
92
|
|
|
93
|
+
These detailed fields are for strict reports and structured receipts. Ordinary advisory work can summarize the next action (or none) without mandatory labels or numbered steps.
|
|
94
|
+
|
|
93
95
|
This is broader than `Remaining Tasks`: it may recommend the next useful work after the current scope is complete. Do not invent filler when nothing meaningful remains. Use `references/next-step.md`; reusable JSON receipts use `yam.next-step.v1`.
|
|
94
96
|
|
|
95
97
|
## Handoff Direction
|
package/references/next-step.md
CHANGED
|
@@ -8,6 +8,8 @@ When code, config, documentation, release metadata, or another project artifact
|
|
|
8
8
|
|
|
9
9
|
## Whole-process scan
|
|
10
10
|
|
|
11
|
+
For ordinary advisory hook work, keep Next step to a concrete remaining action or “no remaining work”; numbered ownership/safety/truth fields are not mandatory. The detailed contract below applies to strict report validation and structured receipts. Do not manufacture a task just to satisfy a report shape.
|
|
12
|
+
|
|
11
13
|
Before writing the sequence, quickly re-check:
|
|
12
14
|
|
|
13
15
|
- the original goal and acceptance criteria;
|
|
@@ -13,6 +13,8 @@ Assess security applicability during new development and changes. Before commit,
|
|
|
13
13
|
|
|
14
14
|
Commit, push, and deployment are distinct actions. A remote CI failure generally blocks protected-branch merge or deployment; it does not mean the preceding Git push was rejected. Local hooks can be bypassed or absent. Do not claim that `--no-verify` is automatically observable; future CI must independently check incoming revisions. Direct deployments must use the same required gate or access restrictions, otherwise enforcement remains incomplete.
|
|
15
15
|
|
|
16
|
+
Branch protection is an acceptance boundary, not authorization to create a branch, worktree or PR. Keep the current branch unless the user or an explicit applicable project rule requests a different workflow. If direct push or merge is blocked, preserve work, report the exact blocker and ask for direction. Do not silently create a PR, change protection settings or bypass the security gate.
|
|
17
|
+
|
|
16
18
|
## Fixed areas and concrete summaries
|
|
17
19
|
|
|
18
20
|
Every run includes each ID exactly once with a concrete explanation, a status, and the applicable evidence or reason. An ID or short heading alone does not satisfy the explanation requirement. These summaries are the canonical minimum; add project-specific targets without removing their security meaning.
|
package/references/study-note.md
CHANGED
|
@@ -16,6 +16,10 @@ This is final-report writing guidance, not a new CLI JSON schema. The current CL
|
|
|
16
16
|
|
|
17
17
|
Keep the default note short: 4-7 lines for ordinary work, longer only for `$deep`, `$mission`, release, DB, runtime, or learning-heavy work.
|
|
18
18
|
|
|
19
|
+
Automatic hook scope is turn-based, not the existing dirty tree. Ordinary work is advisory: summarize the touched artifact/role and change, verification/limits, then a short Next step (or no remaining work). The hook checks only the compact minimum; the fuller guidance below remains useful when relevant. Deep/Mission names alone do not activate strict. Risk intent or explicit/project strict policy requires the detailed contract only if selected artifacts changed. Explicit `yam study-note check` retains strict whole-tree validation.
|
|
20
|
+
|
|
21
|
+
Known generated mission/security receipts, logs and screenshots are not product changes by default; `.yam/study-note.json` includes override excludes. Security evidence is still required by the independent stage security gate. Unavailable or expired baseline means partial/manual inspection, not no changes. Plain, Markdown heading and standalone bold Study Note / Next step titles are accepted.
|
|
22
|
+
|
|
19
23
|
Include:
|
|
20
24
|
|
|
21
25
|
- Touched code/artifact: the file, function, component, config, or artifact that changed.
|
package/skills/deep/SKILL.md
CHANGED
|
@@ -18,6 +18,7 @@ Use for:
|
|
|
18
18
|
|
|
19
19
|
## Principles
|
|
20
20
|
|
|
21
|
+
- Keep the current branch. Create/switch branches, create worktrees or create PRs only on explicit user request or an applicable project rule; commit/push, version bumps, route selection and protection alone do not authorize them. If blocked, report and ask instead of silently changing workflow, protections or bypassing checks.
|
|
21
22
|
- Direction before execution.
|
|
22
23
|
- Token economy still matters, even in deep mode.
|
|
23
24
|
- Reuse `yam.project.md` before broad context reading when present.
|
package/skills/mission/SKILL.md
CHANGED
|
@@ -24,6 +24,7 @@ Do not use for:
|
|
|
24
24
|
|
|
25
25
|
## Principles
|
|
26
26
|
|
|
27
|
+
- Keep the current branch. Create/switch branches, create worktrees or create PRs only on explicit user request or an applicable project rule; commit/push, version bumps, route selection and protection alone do not authorize them. If blocked, report and ask instead of silently changing workflow, protections or bypassing checks.
|
|
27
28
|
- Direction before execution.
|
|
28
29
|
- Mission is explicit-only; never auto-escalate small tasks into mission.
|
|
29
30
|
- Start from the user's approved plan or ask for one if the plan is missing.
|
package/skills/question/SKILL.md
CHANGED
|
@@ -17,6 +17,7 @@ Do not use for:
|
|
|
17
17
|
- Risky verification or broad debugging. Use `$deep`.
|
|
18
18
|
|
|
19
19
|
Principles:
|
|
20
|
+
- Keep the current branch. Create/switch branches, create worktrees or create PRs only on explicit user request or an applicable project rule; commit/push, version bumps, route selection and protection alone do not authorize them. If blocked, report and ask instead of silently changing workflow, protections or bypassing checks.
|
|
20
21
|
- Direction before execution.
|
|
21
22
|
- Answer the question first.
|
|
22
23
|
- Keep context tiny.
|
package/skills/quick/SKILL.md
CHANGED
|
@@ -22,6 +22,7 @@ Do not use for:
|
|
|
22
22
|
|
|
23
23
|
## Principles
|
|
24
24
|
|
|
25
|
+
- Keep the current branch. Create/switch branches, create worktrees or create PRs only on explicit user request or an applicable project rule; commit/push, version bumps, route selection and protection alone do not authorize them. If blocked, report and ask instead of silently changing workflow, protections or bypassing checks.
|
|
25
26
|
- Direction before execution.
|
|
26
27
|
- Context-reuse first.
|
|
27
28
|
- Token economy is part of quality.
|
|
@@ -84,7 +85,7 @@ Keep it compact:
|
|
|
84
85
|
|
|
85
86
|
- What changed or what the scan found.
|
|
86
87
|
- Study Note when code, config, docs, or project artifacts changed: what changed, what role it has, where it runs or is read, what was wrong or missing, before/after behavior, what behavior is expected, one useful syntax/structure insight, what was verified, and what remains uncertain.
|
|
87
|
-
- Next step immediately after Study Note when artifacts changed;
|
|
88
|
+
- Next step immediately after Study Note when artifacts changed; ordinary advisory work uses one concrete action or no remaining work. Use the detailed `references/next-step.md` contract only for risky/strict reports or structured receipts.
|
|
88
89
|
- Architecture hygiene when relevant: flag if the change risks dumping unrelated logic into `page.tsx`, one-off component CSS into `global.css`, or structured product data into broad DB `jsonb` blobs.
|
|
89
90
|
- What was checked.
|
|
90
91
|
- What was skipped, blocked, or still risky.
|
package/skills/scout/SKILL.md
CHANGED
|
@@ -22,6 +22,7 @@ Use for:
|
|
|
22
22
|
|
|
23
23
|
## Principles
|
|
24
24
|
|
|
25
|
+
- Keep the current branch. Create/switch branches, create worktrees or create PRs only on explicit user request or an applicable project rule; commit/push, version bumps, route selection and protection alone do not authorize them. If blocked, report and ask instead of silently changing workflow, protections or bypassing checks.
|
|
25
26
|
- Scout broadly enough to see the field, then compress the judgment.
|
|
26
27
|
- Token economy is part of quality.
|
|
27
28
|
- Reuse `yam.project.md` before broad context reading when present.
|
package/skills/ueye/SKILL.md
CHANGED
|
@@ -22,6 +22,7 @@ Do not use for:
|
|
|
22
22
|
|
|
23
23
|
## Principles
|
|
24
24
|
|
|
25
|
+
- Keep the current branch. Create/switch branches, create worktrees or create PRs only on explicit user request or an applicable project rule; commit/push, version bumps, route selection and protection alone do not authorize them. If blocked, report and ask instead of silently changing workflow, protections or bypassing checks.
|
|
25
26
|
- Direction before execution.
|
|
26
27
|
- Visual evidence before visual claims.
|
|
27
28
|
- Ueye is the tight UX/UI/design route, not Quick with screenshots.
|
package/src/bin/yam.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { studyNoteTurnScope } from '../lib/study-note-scope.js';
|
|
3
4
|
import { selectHookNode, probeHookDiscovery, inspectHookObservation, recordHookObservation } from '../lib/hook-readiness.js';
|
|
4
5
|
import { readMemoryDirectory } from '../lib/memory-records.js';
|
|
5
6
|
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
@@ -988,8 +989,8 @@ async function studyNoteCheck(args = []) {
|
|
|
988
989
|
if (result.blockers.length) process.exitCode = 1;
|
|
989
990
|
}
|
|
990
991
|
|
|
991
|
-
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
|
|
992
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
992
|
+
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided', scope: any = null) {
|
|
993
|
+
const changedFileDetection = scope || gitChangedFileSnapshot(dir);
|
|
993
994
|
const changedFiles = changedFileDetection.files;
|
|
994
995
|
const changeScopeAvailable = changedFileDetection.available;
|
|
995
996
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
@@ -1024,6 +1025,13 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
1024
1025
|
for (const requirement of hygieneRequirements) {
|
|
1025
1026
|
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
1026
1027
|
}
|
|
1028
|
+
if (scope?.mode === 'advisory') {
|
|
1029
|
+
const ordinary = new Set(['changed_files', 'study_note_present', 'role_or_responsibility', 'verification', 'limits_or_uncertainty', 'next_step_present', 'single_study_note_next_step_pair', 'study_note_then_next_step']);
|
|
1030
|
+
for (const check of checks) if (!ordinary.has(check.id)) {
|
|
1031
|
+
check.status = 'skipped';
|
|
1032
|
+
check.truth_status = 'skipped';
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1027
1035
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
1028
1036
|
const result = {
|
|
1029
1037
|
schema: 'yam.study-note-guard.v1',
|
|
@@ -1110,7 +1118,7 @@ function hasNextStepMarker(text = '') {
|
|
|
1110
1118
|
|
|
1111
1119
|
function reportSectionMarkers(text = '') {
|
|
1112
1120
|
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1113
|
-
const trimmed = line.trim();
|
|
1121
|
+
const trimmed = line.trim().replace(/^\*\*(.*?)\*\*\s*:?[ \t]*$/, '$1');
|
|
1114
1122
|
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1115
1123
|
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1116
1124
|
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
@@ -1236,7 +1244,7 @@ function studyNoteHygieneRequirements(files = []) {
|
|
|
1236
1244
|
if (files.some((file) => /(^|\/)global\.css$/i.test(file))) {
|
|
1237
1245
|
requirements.push({ id: 'global_css_hygiene', terms: ['global.css', 'token', 'component', 'scoped', 'module', 'utility'], note: 'Study Note should say whether CSS belongs globally or should move to tokens/scoped/component styles' });
|
|
1238
1246
|
}
|
|
1239
|
-
if (files.some((file) => /(
|
|
1247
|
+
if (files.some((file) => /\.(sql|prisma)$/i.test(file) || /(^|\/)(migrations?|schema|database|drizzle|supabase)\//i.test(file) && /\.(ts|js)$/i.test(file))) {
|
|
1240
1248
|
requirements.push({ id: 'jsonb_hygiene', terms: ['jsonb', 'column', 'table', 'constraint', 'index', 'schema', 'relation'], note: 'Study Note should say whether structured product data belongs in typed columns/tables/constraints/indexes instead of broad DB jsonb' });
|
|
1241
1249
|
}
|
|
1242
1250
|
return requirements;
|
|
@@ -2465,7 +2473,7 @@ async function hookRun(args = []) {
|
|
|
2465
2473
|
}
|
|
2466
2474
|
const prompt = extractPrompt(input);
|
|
2467
2475
|
const additionalContext = profile === 'study-note'
|
|
2468
|
-
? await
|
|
2476
|
+
? await buildStudyNoteTurnContext(input, cwd)
|
|
2469
2477
|
: await buildYamLiteContext({ cwd, prompt });
|
|
2470
2478
|
const output = {
|
|
2471
2479
|
continue: true,
|
|
@@ -2497,6 +2505,7 @@ function extractPrompt(input: AnyRecord = {}) {
|
|
|
2497
2505
|
async function buildYamLiteContext({ cwd, prompt }) {
|
|
2498
2506
|
const lines = [
|
|
2499
2507
|
'yam-lite guide active: keep project direction visible, preserve momentum, and deepen when scope, risk, or user intent calls for it.',
|
|
2508
|
+
'Git workflow: keep the current branch; create/switch branches, worktrees or PRs only on explicit user request or applicable project rule. Commit/push, release bumps, branch-name prefixes and protection alone do not authorize them. If blocked, report and ask; do not silently change workflow or protections.',
|
|
2500
2509
|
'Default: do a basic direction-fit and honest-verification check; avoid broad reading for small work and never claim proof without evidence.'
|
|
2501
2510
|
];
|
|
2502
2511
|
const pack = await findProjectPack(cwd).catch(() => null);
|
|
@@ -2512,38 +2521,10 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
2512
2521
|
return lines.join('\n');
|
|
2513
2522
|
}
|
|
2514
2523
|
|
|
2515
|
-
async function buildStudyNoteHookContext({ cwd }) {
|
|
2516
|
-
const dir = path.resolve(cwd || process.cwd());
|
|
2517
|
-
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2518
|
-
const changedFiles = changedFileDetection.files;
|
|
2519
|
-
const lines = [
|
|
2520
|
-
'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.'
|
|
2521
|
-
];
|
|
2522
|
-
if (!changedFileDetection.available) {
|
|
2523
|
-
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2524
|
-
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.');
|
|
2525
|
-
return lines.join('\n');
|
|
2526
|
-
}
|
|
2527
|
-
if (!changedFiles.length) {
|
|
2528
|
-
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.');
|
|
2529
|
-
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2530
|
-
return lines.join('\n');
|
|
2531
|
-
}
|
|
2532
|
-
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2533
|
-
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2534
|
-
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.');
|
|
2535
|
-
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2536
|
-
if (hygiene.length) {
|
|
2537
|
-
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2538
|
-
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.');
|
|
2539
|
-
}
|
|
2540
|
-
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.');
|
|
2541
|
-
return lines.join('\n');
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
2524
|
async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd()) {
|
|
2545
2525
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
2546
|
-
const
|
|
2526
|
+
const scope = studyNoteTurnScope(input, cwd);
|
|
2527
|
+
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message', scope);
|
|
2547
2528
|
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2548
2529
|
return {
|
|
2549
2530
|
continue: true,
|
|
@@ -2551,7 +2532,11 @@ async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd
|
|
|
2551
2532
|
};
|
|
2552
2533
|
}
|
|
2553
2534
|
if (!result.blockers.length) return { continue: true };
|
|
2554
|
-
const
|
|
2535
|
+
const failed = result.checks.filter(check => check.status === 'fail').map(check => check.id);
|
|
2536
|
+
const summary = failed.includes('study_note_present') ? 'study_note_present' : failed.join(', ');
|
|
2537
|
+
if (scope.mode !== 'strict') {
|
|
2538
|
+
return { continue: true, systemMessage: `yam Study Note advisory — missing fields: ${summary}. Keep ordinary work concise; no correction gate applied.` };
|
|
2539
|
+
}
|
|
2555
2540
|
if (Boolean(input?.stop_hook_active || input?.stopHookActive)) {
|
|
2556
2541
|
return {
|
|
2557
2542
|
continue: true,
|
|
@@ -2561,13 +2546,18 @@ async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd
|
|
|
2561
2546
|
return {
|
|
2562
2547
|
decision: 'block',
|
|
2563
2548
|
reason: [
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
'Revise the final response to include: touched artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, limits, and any relevant architecture hygiene.'
|
|
2549
|
+
`yam Study Note strict — missing fields: ${summary}.`,
|
|
2550
|
+
'Revise only the missing fields in Study Note / Next step; this is report validation, not a security scan.'
|
|
2567
2551
|
].join(' ')
|
|
2568
2552
|
};
|
|
2569
2553
|
}
|
|
2570
2554
|
|
|
2555
|
+
async function buildStudyNoteTurnContext(input: AnyRecord, cwd: string) {
|
|
2556
|
+
const scope = studyNoteTurnScope(input, cwd, true);
|
|
2557
|
+
if (!scope.available) return 'yam Study Note: turn scope is unavailable; do not treat this as a clean project. Inspect changed artifacts manually; no strict block will be applied without a baseline.';
|
|
2558
|
+
return `yam Study Note turn baseline saved (hash-only). Mode: ${scope.mode}. Only changes since this prompt count; known generated logs, mission/security receipts and screenshots are excluded unless explicitly included. Include a concise Study Note followed by Next step when product artifacts change. ${scope.mode === 'strict' ? 'Risk work uses the detailed report contract and one correction pass.' : 'Ordinary work is advisory: a short explanation, verification/limits and remaining work (or none) suffice.'}`;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2571
2561
|
function yamLiteRouteHint(prompt = '') {
|
|
2572
2562
|
const text = String(prompt || '').toLowerCase();
|
|
2573
2563
|
if (!text) return '';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const hash = (value: string | Buffer) => createHash('sha256').update(value).digest('hex');
|
|
8
|
+
const MAX_FILES = 4000;
|
|
9
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
type Snapshot = { root: string; files: Record<string, string> };
|
|
11
|
+
type Policy = { include: string[]; exclude: string[]; strict: boolean };
|
|
12
|
+
|
|
13
|
+
function git(dir: string, args: string[]) {
|
|
14
|
+
const result = spawnSync('git', args, { cwd: dir, encoding: 'utf8', timeout: 750, maxBuffer: MAX_BYTES,
|
|
15
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } });
|
|
16
|
+
if (result.status !== 0) throw new Error('git_scope_unavailable');
|
|
17
|
+
return result.stdout;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function snapshot(dir: string): Snapshot {
|
|
21
|
+
const root = fs.realpathSync(git(dir, ['rev-parse', '--show-toplevel']).trim());
|
|
22
|
+
const config = policy(root);
|
|
23
|
+
const files: Record<string, string> = Object.create(null);
|
|
24
|
+
let bytesRead = 0;
|
|
25
|
+
const tracked = git(root, ['ls-files', '--stage', '-z']).split('\0').filter(Boolean);
|
|
26
|
+
for (const row of tracked) {
|
|
27
|
+
const tab = row.indexOf('\t');
|
|
28
|
+
if (selected(row.slice(tab + 1), config)) files[row.slice(tab + 1)] = hash(row.slice(0, tab));
|
|
29
|
+
}
|
|
30
|
+
const rows = git(root, ['status', '--short', '-z', '--untracked-files=all']).split('\0');
|
|
31
|
+
for (let i = 0; i < rows.length; i++) {
|
|
32
|
+
const row = rows[i];
|
|
33
|
+
if (!row) continue;
|
|
34
|
+
const name = row.slice(3);
|
|
35
|
+
if (/[RC]/.test(row.slice(0, 2))) i++;
|
|
36
|
+
if (!selected(name, config)) continue;
|
|
37
|
+
const target = path.resolve(root, name);
|
|
38
|
+
if (!target.startsWith(root + path.sep)) throw new Error('unsafe_path');
|
|
39
|
+
// Do not follow symlinks, including symlinked parent directories.
|
|
40
|
+
let parent = path.dirname(target);
|
|
41
|
+
while (parent !== root) {
|
|
42
|
+
try { if (fs.lstatSync(parent).isSymbolicLink()) throw new Error('unsafe_path'); }
|
|
43
|
+
catch (error: any) { if (error.code !== 'ENOENT') throw error; }
|
|
44
|
+
parent = path.dirname(parent);
|
|
45
|
+
}
|
|
46
|
+
let content = 'missing';
|
|
47
|
+
try {
|
|
48
|
+
const stat = fs.lstatSync(target);
|
|
49
|
+
if (stat.isSymbolicLink()) content = 'link:' + hash(fs.readlinkSync(target));
|
|
50
|
+
else if (stat.isFile() && (bytesRead += stat.size) <= MAX_BYTES) content = hash(fs.readFileSync(target));
|
|
51
|
+
else throw new Error('scope_limit');
|
|
52
|
+
} catch (error: any) { if (error.code !== 'ENOENT') throw error; }
|
|
53
|
+
files[name] = hash(JSON.stringify([row.slice(0, 2), files[name] || '', content]));
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(files).length > MAX_FILES) throw new Error('scope_limit');
|
|
56
|
+
return { root, files };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function policy(root: string): Policy {
|
|
60
|
+
const target = path.join(root, '.yam', 'study-note.json');
|
|
61
|
+
let value: any = {};
|
|
62
|
+
if (fs.existsSync(target)) {
|
|
63
|
+
if (fs.lstatSync(path.dirname(target)).isSymbolicLink() || fs.lstatSync(target).isSymbolicLink()
|
|
64
|
+
|| fs.statSync(target).size > 16384) throw new Error('invalid_policy');
|
|
65
|
+
value = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
66
|
+
if (!value || !['advisory', 'strict', undefined].includes(value.mode)) throw new Error('invalid_policy');
|
|
67
|
+
}
|
|
68
|
+
const list = (key: string) => {
|
|
69
|
+
const result = value[key] || [];
|
|
70
|
+
if (!Array.isArray(result) || result.length > 100 || result.some((x: any) => typeof x !== 'string'
|
|
71
|
+
|| !x || x.startsWith('/') || x.includes('..') || /[\\\0]/.test(x) || x.replace(/\/\*\*$/, '').includes('*'))) throw new Error('invalid_policy');
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
return { include: list('include'), exclude: list('exclude'), strict: value.mode === 'strict' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const matches = (name: string, pattern: string) => pattern.endsWith('/**')
|
|
78
|
+
? name.startsWith(pattern.slice(0, -2)) : name === pattern;
|
|
79
|
+
function selected(name: string, config: Policy) {
|
|
80
|
+
if (config.include.some(p => matches(name, p))) return true;
|
|
81
|
+
if (config.exclude.some(p => matches(name, p))) return false;
|
|
82
|
+
return !/^(dist\/|\.yam\/(mission|security|logs|screenshots)\/)/.test(name)
|
|
83
|
+
&& !/\.tgz$/.test(name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function statePath(root: string, session: string) {
|
|
87
|
+
const dir = path.join(os.tmpdir(), `yam-study-note-${process.getuid?.() ?? 'user'}`);
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
89
|
+
const stat = fs.lstatSync(dir);
|
|
90
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid())
|
|
91
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077))) throw new Error('unsafe_state');
|
|
92
|
+
return path.join(dir, hash(root + '\0' + session) + '.json');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Hash-only, bounded scope evidence; not a security attestation. */
|
|
96
|
+
export function studyNoteTurnScope(input: any, cwd: string, begin = false) {
|
|
97
|
+
try {
|
|
98
|
+
const session = String(input.session_id || input.sessionId || input.thread_id || '');
|
|
99
|
+
const turn = String(input.turn_id || input.turnId || '');
|
|
100
|
+
if (!session || session.length > 512 || turn.length > 512) throw new Error('session_unavailable');
|
|
101
|
+
const current = snapshot(cwd);
|
|
102
|
+
const config = policy(current.root);
|
|
103
|
+
const target = statePath(current.root, session);
|
|
104
|
+
if (begin) {
|
|
105
|
+
const prompt = String(input.prompt || input.user_prompt || '');
|
|
106
|
+
// Deep/Mission alone do not make a read-only task strict. Risk intent or explicit strict does.
|
|
107
|
+
const strict = config.strict || /(?:study-note\s+strict|strict\s+study.note|commit|push|deploy|publish|security|migration|database|\bDB\b|커밋|푸시|배포|보안|마이그레이션|데이터베이스)/i.test(prompt);
|
|
108
|
+
const state = { schema: 'yam.study-note-baseline.v1', created: Date.now(), turn, strict, ...current };
|
|
109
|
+
try {
|
|
110
|
+
const previous = fs.lstatSync(target);
|
|
111
|
+
if (!previous.isFile() || previous.isSymbolicLink() || previous.nlink !== 1
|
|
112
|
+
|| (process.getuid && previous.uid !== process.getuid())) throw new Error('unsafe_state');
|
|
113
|
+
} catch (error: any) { if (error.code !== 'ENOENT') throw error; }
|
|
114
|
+
const fd = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW || 0), 0o600);
|
|
115
|
+
try { fs.writeFileSync(fd, JSON.stringify(state)); } finally { fs.closeSync(fd); }
|
|
116
|
+
return { available: true, files: [], source: 'turn_baseline', mode: strict ? 'strict' : 'advisory', reason: '' };
|
|
117
|
+
}
|
|
118
|
+
const stat = fs.lstatSync(target);
|
|
119
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_BYTES) throw new Error('invalid_baseline');
|
|
120
|
+
const before = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
121
|
+
if (before.schema !== 'yam.study-note-baseline.v1' || before.root !== current.root
|
|
122
|
+
|| !before.files || typeof before.files !== 'object' || Array.isArray(before.files)
|
|
123
|
+
|| !Number.isFinite(before.created) || typeof before.strict !== 'boolean'
|
|
124
|
+
|| Object.keys(before.files).length > MAX_FILES
|
|
125
|
+
|| Object.values(before.files).some(value => typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value))
|
|
126
|
+
|| Date.now() - before.created > 24 * 60 * 60 * 1000 || before.created > Date.now()
|
|
127
|
+
|| (turn && before.turn !== turn)) throw new Error('stale_baseline');
|
|
128
|
+
const files = [...new Set([...Object.keys(before.files), ...Object.keys(current.files)])]
|
|
129
|
+
.filter(name => before.files[name] !== current.files[name] && selected(name, config));
|
|
130
|
+
return { available: true, files, source: 'turn_baseline', mode: config.strict || before.strict ? 'strict' : 'advisory', reason: '' };
|
|
131
|
+
} catch {
|
|
132
|
+
return { available: false, files: [], source: 'turn_baseline', mode: 'advisory', reason: 'baseline_unavailable; inspect changed artifacts manually' };
|
|
133
|
+
}
|
|
134
|
+
}
|
package/yam.manifest.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yam",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
4
4
|
"principles": [
|
|
5
5
|
"Direction before execution.",
|
|
6
|
+
"Keep the current branch; new/switched branches, worktrees and PRs require explicit user direction or applicable project rules. Protection blockers require reporting, not silent workflow changes or bypass.",
|
|
6
7
|
"Start fast.",
|
|
7
8
|
"Deepen deliberately.",
|
|
8
9
|
"Basic direction fit and honest verification always.",
|
|
@@ -13,12 +14,12 @@
|
|
|
13
14
|
"Context-reuse first.",
|
|
14
15
|
"End with remaining tasks and fix-first items.",
|
|
15
16
|
"When code, config, release metadata, documentation, or project artifacts changed, include a Study Note even if no yam skill was explicitly invoked.",
|
|
16
|
-
"When the opt-in Study Note hook is enabled,
|
|
17
|
+
"When the opt-in Study Note hook is enabled, compare against a hash-only turn baseline; ordinary work is advisory and selected risky changes request at most one strict correction pass.",
|
|
17
18
|
"Explain what changed, what role it has, where it runs or is read, what was wrong or missing when relevant, before/after behavior, expected behavior, and one useful syntax or structure insight in short non-specialist language.",
|
|
18
19
|
"When relevant, include architecture hygiene: avoid dumping unrelated logic into page.tsx, one-off component CSS into global.css, or structured product data into broad DB jsonb blobs when better project boundaries exist.",
|
|
19
20
|
"Use handoff notes for fix-first items, remaining tasks, recommended next direction, requirement coverage, blocker kind, failure cause, recovery hint, safe retry, evidence level, and owner scope when useful.",
|
|
20
21
|
"Use bounded heavy verification for serious claims, with explicit stop conditions instead of endless proof loops.",
|
|
21
|
-
"After changed artifacts, put
|
|
22
|
+
"After changed artifacts, put Next step immediately after Study Note; ordinary work stays brief, while strict reports use evidence-bound whole-process scanning and fix-first-before-planned ordering."
|
|
22
23
|
],
|
|
23
24
|
"defaultHooks": false,
|
|
24
25
|
"routes": [
|