yam-harness 0.1.0

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.
Files changed (43) hide show
  1. package/AGENTS.md +18 -0
  2. package/COMMANDS.md +144 -0
  3. package/DECISIONS.md +70 -0
  4. package/LICENSE +21 -0
  5. package/README.md +159 -0
  6. package/ROADMAP.md +308 -0
  7. package/bin/yam.js +1966 -0
  8. package/package.json +74 -0
  9. package/references/context-reuse.md +59 -0
  10. package/references/current-docs.md +45 -0
  11. package/references/db-supabase-safety-lite.md +40 -0
  12. package/references/doctor-scan.md +56 -0
  13. package/references/eye.md +30 -0
  14. package/references/final-report.md +61 -0
  15. package/references/honest-completion.md +61 -0
  16. package/references/hook-lite.md +55 -0
  17. package/references/markdown-management.md +56 -0
  18. package/references/memory.md +59 -0
  19. package/references/mission.md +86 -0
  20. package/references/question.md +25 -0
  21. package/references/quick.md +70 -0
  22. package/references/risk-escalation.md +27 -0
  23. package/references/runtime-orchestration.md +57 -0
  24. package/references/scout.md +38 -0
  25. package/references/token-budget-reporter.md +44 -0
  26. package/references/token-economy.md +61 -0
  27. package/references/tool-trust-layer.md +113 -0
  28. package/references/truth-matrix.md +44 -0
  29. package/references/ueye.md +83 -0
  30. package/references/ui-quality.md +23 -0
  31. package/references/verification-levels.md +53 -0
  32. package/skills/deep/SKILL.md +76 -0
  33. package/skills/mission/SKILL.md +105 -0
  34. package/skills/question/SKILL.md +45 -0
  35. package/skills/quick/SKILL.md +81 -0
  36. package/skills/scout/SKILL.md +71 -0
  37. package/skills/ueye/SKILL.md +90 -0
  38. package/templates/mission-plan.md +46 -0
  39. package/templates/runtime-proof.md +54 -0
  40. package/templates/tuning-log.md +39 -0
  41. package/templates/ueye-review.md +62 -0
  42. package/templates/yam.project.md +71 -0
  43. package/yam.manifest.json +48 -0
package/bin/yam.js ADDED
@@ -0,0 +1,1966 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import fsp from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const SKILLS = [
11
+ 'quick',
12
+ 'ueye',
13
+ 'question',
14
+ 'scout',
15
+ 'deep',
16
+ 'mission'
17
+ ];
18
+ const LEGACY_SKILLS = SKILLS.flatMap((skill) => [`timeto-${skill}`, `yam-${skill}`]);
19
+ const RETIRED_SKILLS = [
20
+ 'runtime',
21
+ 'fast',
22
+ 'build',
23
+ 'ui',
24
+ 'review',
25
+ 'eye',
26
+ 'timeto-runtime',
27
+ 'yam-runtime',
28
+ 'timeto-fast',
29
+ 'yam-fast',
30
+ 'timeto-build',
31
+ 'yam-build',
32
+ 'timeto-ui',
33
+ 'yam-ui',
34
+ 'timeto-review',
35
+ 'yam-review',
36
+ 'timeto-eye',
37
+ 'yam-eye'
38
+ ];
39
+
40
+ const DEST = process.env.YAM_SKILLS_HOME || process.env.TIMETO_SKILLS_HOME || path.join(os.homedir(), '.agents', 'skills');
41
+ const CODEX_MIRROR = process.env.YAM_CODEX_MIRROR || process.env.TIMETO_CODEX_MIRROR || path.join(os.homedir(), '.codex', 'skills');
42
+ const VERSION = '0.1.0';
43
+ const PROJECT_PACK = 'yam.project.md';
44
+ const LEGACY_PROJECT_PACK = 'timeto.project.md';
45
+ const PACK_STALE_DAYS = 30;
46
+ const YAM_LITE_HOOK_COMMAND = `node ${path.join(ROOT, 'bin', 'yam.js')} hook run lite`;
47
+ const REQUIRED_PACK_SECTIONS = [
48
+ 'Product Direction',
49
+ 'UI Direction',
50
+ 'Tech Stack',
51
+ 'Commands',
52
+ 'Key Paths',
53
+ 'Verification Policy',
54
+ 'Known Risks',
55
+ 'Recent Decisions',
56
+ 'No-Go Rules',
57
+ 'MD Management'
58
+ ];
59
+ const ROUTE_BUDGETS = {
60
+ quick: {
61
+ files: '1-8 files depending on lane; start with 1-3 for patch work',
62
+ commands: '0-2 focused checks; prefer the smallest honest type/lint/test/build signal',
63
+ report: 'short change or scan summary plus a compact verification matrix when useful',
64
+ expand: 'only when the first edit surface is wrong, error grouping points wider, or verification contradicts the hypothesis',
65
+ limits: { files: 8, commands: 2, reportLines: 16, seconds: 300 }
66
+ },
67
+ ueye: {
68
+ files: 'project direction, target UI surface, visual evidence, nearby component/styles',
69
+ commands: 'browser/screenshot when feasible; inspect 1-3 primary images by default; typecheck/build only if UI implementation changed code',
70
+ report: 'design work, source evidence, max 5 inventory rows, states checked, P0-P3 ledger, before/after, truth cap',
71
+ expand: 'when direction, reference image, or visual evidence requires it; do not do broad design archaeology for simple tweaks',
72
+ limits: { files: 10, commands: 3, reportLines: 28, seconds: 600 }
73
+ },
74
+ question: {
75
+ files: '0-2 files or current conversation context',
76
+ commands: 'none by default',
77
+ report: 'direct answer, usually 1-6 short paragraphs or bullets',
78
+ expand: 'switch to scout when sources, comparisons, or freshness matter',
79
+ limits: { files: 2, commands: 0, reportLines: 10, seconds: 120 }
80
+ },
81
+ scout: {
82
+ files: 'project pack plus 3-7 high-signal sources',
83
+ commands: 'none by default',
84
+ report: 'decision, objective/subjective judgment, risks, sources',
85
+ expand: 'only when the decision remains uncertain',
86
+ limits: { files: 8, commands: 0, reportLines: 24, seconds: 600 }
87
+ },
88
+ deep: {
89
+ files: 'risk surface plus dependencies; runtime context only when needed',
90
+ commands: 'test/build/browser/security/runtime checks as needed',
91
+ report: 'evidence, truth status, cleanup if applicable, residual risk',
92
+ expand: 'allowed when tied to risk or runtime proof',
93
+ limits: { files: 25, commands: 8, reportLines: 40, seconds: 1800 }
94
+ },
95
+ mission: {
96
+ files: 'approved plan, project pack, role-specific surfaces, runtime context if needed',
97
+ commands: 'focused checks plus deep runtime/browser/tmux checks when needed',
98
+ report: 'real subagent/team lanes, cross-verification, doctor scan, evidence, cleanup, truth status',
99
+ expand: 'allowed for approved broad implementation plans that use real subagent/team execution',
100
+ limits: { files: 30, commands: 10, reportLines: 48, seconds: 2400 }
101
+ }
102
+ };
103
+
104
+ function usage() {
105
+ console.log(`yam ${VERSION}
106
+
107
+ Usage:
108
+ yam list
109
+ yam status
110
+ yam verify
111
+ yam detect [dir]
112
+ yam pack [dir]
113
+ yam budget [route]
114
+ yam measure <route> [--files n] [--commands n] [--report-lines n] [--seconds n]
115
+ yam tools doctor [dir]
116
+ yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
117
+ yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
118
+ yam safety [text...]
119
+ yam memory <init|add|list|summary|resolve> [dir] [options]
120
+ yam hook <status|enable|disable|run> [lite] [--global|--project dir]
121
+ yam template <project|ueye|mission|proof|tuning>
122
+ yam tune-log [dir]
123
+ yam install
124
+ yam uninstall
125
+ yam doctor
126
+ yam examples
127
+ yam path
128
+ yam version
129
+ yam init-project [dir]
130
+
131
+ Environment:
132
+ YAM_SKILLS_HOME Override install target. Default: ~/.agents/skills
133
+ `);
134
+ }
135
+
136
+ async function exists(file) {
137
+ try {
138
+ await fsp.access(file);
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ async function rmrf(target) {
146
+ await fsp.rm(target, { recursive: true, force: true });
147
+ }
148
+
149
+ async function copyDir(source, target) {
150
+ await fsp.mkdir(target, { recursive: true });
151
+ const entries = await fsp.readdir(source, { withFileTypes: true });
152
+ for (const entry of entries) {
153
+ const from = path.join(source, entry.name);
154
+ const to = path.join(target, entry.name);
155
+ if (entry.isDirectory()) {
156
+ await copyDir(from, to);
157
+ } else if (entry.isFile()) {
158
+ await fsp.copyFile(from, to);
159
+ }
160
+ }
161
+ }
162
+
163
+ async function installSkill(skill) {
164
+ const source = path.join(ROOT, 'skills', skill);
165
+ const target = path.join(DEST, skill);
166
+ const references = path.join(ROOT, 'references');
167
+
168
+ if (!await exists(path.join(source, 'SKILL.md'))) {
169
+ throw new Error(`missing skill source: ${source}`);
170
+ }
171
+
172
+ await rmrf(target);
173
+ await fsp.mkdir(target, { recursive: true });
174
+ await fsp.copyFile(path.join(source, 'SKILL.md'), path.join(target, 'SKILL.md'));
175
+ await copyDir(references, path.join(target, 'references'));
176
+ }
177
+
178
+ async function install() {
179
+ await fsp.mkdir(DEST, { recursive: true });
180
+ for (const skill of SKILLS) {
181
+ await installSkill(skill);
182
+ }
183
+ for (const legacySkill of LEGACY_SKILLS) {
184
+ await rmrf(path.join(DEST, legacySkill));
185
+ }
186
+ for (const retiredSkill of RETIRED_SKILLS) {
187
+ await rmrf(path.join(DEST, retiredSkill));
188
+ }
189
+ if (CODEX_MIRROR !== DEST && fs.existsSync(CODEX_MIRROR)) {
190
+ for (const skill of [...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
191
+ await rmrf(path.join(CODEX_MIRROR, skill));
192
+ }
193
+ }
194
+ console.log(`yam installed to ${DEST}`);
195
+ console.log('Restart Codex to reload skills.');
196
+ }
197
+
198
+ async function uninstall() {
199
+ for (const skill of [...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
200
+ await rmrf(path.join(DEST, skill));
201
+ if (CODEX_MIRROR !== DEST) {
202
+ await rmrf(path.join(CODEX_MIRROR, skill));
203
+ }
204
+ }
205
+ console.log(`yam removed from ${DEST}`);
206
+ if (CODEX_MIRROR !== DEST) console.log(`yam mirror entries removed from ${CODEX_MIRROR}`);
207
+ console.log('Restart Codex to unload skills.');
208
+ }
209
+
210
+ async function status({ quiet = false } = {}) {
211
+ let missing = 0;
212
+ for (const skill of SKILLS) {
213
+ const ok = await exists(path.join(DEST, skill, 'SKILL.md')) && await exists(path.join(DEST, skill, 'references'));
214
+ if (!quiet) console.log(`${ok ? 'ok ' : 'missing'} ${skill}`);
215
+ if (!ok) missing += 1;
216
+ }
217
+ return missing;
218
+ }
219
+
220
+ async function list() {
221
+ const manifest = await loadManifest();
222
+ console.log(`${manifest.name} ${manifest.version}`);
223
+ for (const principle of manifest.principles) console.log(`- ${principle}`);
224
+ console.log('\nRoutes:');
225
+ for (const route of manifest.routes) {
226
+ console.log(`- $${route.id} [${route.stage}] ${route.purpose}`);
227
+ }
228
+ }
229
+
230
+ async function loadManifest() {
231
+ return JSON.parse(await fsp.readFile(path.join(ROOT, 'yam.manifest.json'), 'utf8'));
232
+ }
233
+
234
+ async function readJson(file) {
235
+ return JSON.parse(await fsp.readFile(file, 'utf8'));
236
+ }
237
+
238
+ async function readJsonOrDefault(file, fallback = {}) {
239
+ try {
240
+ return JSON.parse(await fsp.readFile(file, 'utf8'));
241
+ } catch {
242
+ return fallback;
243
+ }
244
+ }
245
+
246
+ async function readText(file) {
247
+ return fsp.readFile(file, 'utf8');
248
+ }
249
+
250
+ function frontmatterName(text) {
251
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
252
+ if (!match) return null;
253
+ const name = match[1].match(/^name:\s*([^\n]+)\s*$/m);
254
+ return name ? name[1].trim().replace(/^["']|["']$/g, '') : null;
255
+ }
256
+
257
+ async function verify({ quiet = false } = {}) {
258
+ const issues = [];
259
+ const manifest = await loadManifest().catch((error) => {
260
+ issues.push(`manifest unreadable: ${error.message}`);
261
+ return null;
262
+ });
263
+
264
+ if (!manifest) return failOrReport('yam verify', issues, quiet);
265
+ if (manifest.name !== 'yam') issues.push('manifest name must be yam');
266
+ if (manifest.defaultHooks !== false) issues.push('manifest defaultHooks must be false');
267
+ if (!Array.isArray(manifest.routes) || manifest.routes.length !== SKILLS.length) {
268
+ issues.push(`manifest routes must contain ${SKILLS.length} routes`);
269
+ }
270
+
271
+ const routeIds = new Set((manifest.routes || []).map((route) => route.id));
272
+ for (const skill of SKILLS) {
273
+ if (!routeIds.has(skill)) issues.push(`manifest missing route: ${skill}`);
274
+ const skillPath = path.join(ROOT, 'skills', skill, 'SKILL.md');
275
+ if (!await exists(skillPath)) {
276
+ issues.push(`missing source skill: ${skill}`);
277
+ continue;
278
+ }
279
+ const text = await readText(skillPath);
280
+ const name = frontmatterName(text);
281
+ if (name !== skill) issues.push(`${skill} frontmatter name mismatch: ${name || 'missing'}`);
282
+ if (!/Direction before execution\./.test(text) && !['scout'].includes(skill)) {
283
+ issues.push(`${skill} missing Direction before execution principle`);
284
+ }
285
+ }
286
+
287
+ const requiredReferences = [
288
+ 'verification-levels.md',
289
+ 'truth-matrix.md',
290
+ 'honest-completion.md',
291
+ 'risk-escalation.md',
292
+ 'quick.md',
293
+ 'ueye.md',
294
+ 'ui-quality.md',
295
+ 'question.md',
296
+ 'mission.md',
297
+ 'doctor-scan.md',
298
+ 'scout.md',
299
+ 'runtime-orchestration.md',
300
+ 'hook-lite.md',
301
+ 'tool-trust-layer.md',
302
+ 'db-supabase-safety-lite.md',
303
+ 'current-docs.md',
304
+ 'token-economy.md',
305
+ 'context-reuse.md',
306
+ 'markdown-management.md',
307
+ 'final-report.md',
308
+ 'token-budget-reporter.md',
309
+ 'memory.md'
310
+ ];
311
+ for (const ref of requiredReferences) {
312
+ if (!await exists(path.join(ROOT, 'references', ref))) issues.push(`missing reference: ${ref}`);
313
+ }
314
+ if (!await exists(path.join(ROOT, 'templates', PROJECT_PACK))) {
315
+ issues.push(`missing template: ${PROJECT_PACK}`);
316
+ } else {
317
+ const projectTemplate = await readText(path.join(ROOT, 'templates', PROJECT_PACK));
318
+ for (const section of REQUIRED_PACK_SECTIONS) {
319
+ if (!hasHeading(projectTemplate, section)) issues.push(`project template missing section: ${section}`);
320
+ }
321
+ }
322
+ for (const template of ['ueye-review.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md']) {
323
+ if (!await exists(path.join(ROOT, 'templates', template))) issues.push(`missing template: ${template}`);
324
+ }
325
+
326
+ const forbiddenPaths = [
327
+ path.join(ROOT, '.codex', 'hooks.json'),
328
+ path.join(ROOT, '.agents', 'hooks.json'),
329
+ path.join(ROOT, 'hooks.json')
330
+ ];
331
+ for (const forbidden of forbiddenPaths) {
332
+ if (await exists(forbidden)) issues.push(`unexpected hook file: ${forbidden}`);
333
+ }
334
+
335
+ const installedMissing = await status({ quiet: true });
336
+ if (installedMissing > 0) issues.push(`${installedMissing} installed skill(s) missing or incomplete`);
337
+
338
+ return failOrReport('yam verify', issues, quiet);
339
+ }
340
+
341
+ function failOrReport(label, issues, quiet = false) {
342
+ if (!issues.length) {
343
+ if (!quiet) console.log(`${label}: ok`);
344
+ return 0;
345
+ }
346
+ if (!quiet) {
347
+ console.log(`${label}: issues`);
348
+ for (const issue of issues) console.log(`- ${issue}`);
349
+ }
350
+ process.exitCode = 1;
351
+ return issues.length;
352
+ }
353
+
354
+ async function doctor() {
355
+ const issues = [];
356
+ for (const skill of SKILLS) {
357
+ if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md'))) issues.push(`missing source skill: ${skill}`);
358
+ }
359
+ if (!await exists(path.join(ROOT, 'references', 'truth-matrix.md'))) issues.push('missing truth matrix reference');
360
+ if (await exists(path.join(ROOT, '.codex', 'hooks.json'))) issues.push('unexpected local hooks.json');
361
+ if (await exists(path.join(os.homedir(), '.codex', 'automations', 'yam'))) issues.push('unexpected yam automation');
362
+ if (await exists(path.join(os.homedir(), '.codex', 'automations', 'timeto'))) issues.push('unexpected legacy timeto automation');
363
+ for (const legacySkill of LEGACY_SKILLS) {
364
+ if (await exists(path.join(DEST, legacySkill, 'SKILL.md'))) issues.push(`unexpected legacy installed skill: ${legacySkill}`);
365
+ if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, legacySkill, 'SKILL.md'))) {
366
+ issues.push(`unexpected legacy mirror skill: ${legacySkill}`);
367
+ }
368
+ }
369
+ for (const retiredSkill of RETIRED_SKILLS) {
370
+ if (await exists(path.join(DEST, retiredSkill, 'SKILL.md'))) issues.push(`unexpected retired installed skill: ${retiredSkill}`);
371
+ if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, retiredSkill, 'SKILL.md'))) {
372
+ issues.push(`unexpected retired mirror skill: ${retiredSkill}`);
373
+ }
374
+ }
375
+ const missingInstalled = await status({ quiet: true });
376
+ if (missingInstalled > 0) issues.push(`${missingInstalled} installed skill(s) missing or incomplete`);
377
+ const verifyIssues = await verify({ quiet: true });
378
+ if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
379
+
380
+ if (!issues.length) {
381
+ console.log('yam doctor: ok');
382
+ const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
383
+ console.log('No hooks, automations, or global config are required.');
384
+ console.log(`yam-lite hook: ${hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'} (optional)`);
385
+ return;
386
+ }
387
+
388
+ console.log('yam doctor: issues');
389
+ for (const issue of issues) console.log(`- ${issue}`);
390
+ process.exitCode = 1;
391
+ }
392
+
393
+ async function tools(args = []) {
394
+ const subcommand = args[0] || 'doctor';
395
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return toolsUsage();
396
+ if (subcommand === 'doctor') return toolsDoctor(args.slice(1));
397
+ console.error(`unknown tools command: ${subcommand}`);
398
+ return toolsUsage();
399
+ }
400
+
401
+ function toolsUsage() {
402
+ console.log(`yam tools
403
+
404
+ Usage:
405
+ yam tools doctor [dir] [--json]
406
+
407
+ Notes:
408
+ Read-only readiness scan for Codex/App, tmux, browser, Context7, Supabase, and Vercel surfaces.
409
+ It does not install, authenticate, deploy, query databases, or start processes.
410
+ `);
411
+ }
412
+
413
+ function parseToolsDoctorArgs(args = []) {
414
+ const result = { dir: process.cwd(), json: false };
415
+ for (const arg of args) {
416
+ if (arg === '--json') {
417
+ result.json = true;
418
+ } else if (looksLikeDirectoryArg(arg)) {
419
+ result.dir = arg;
420
+ }
421
+ }
422
+ return result;
423
+ }
424
+
425
+ async function toolsDoctor(args = []) {
426
+ const parsed = Array.isArray(args) ? parseToolsDoctorArgs(args) : { dir: args || process.cwd(), json: false };
427
+ const data = await buildToolsDoctorReport(parsed.dir);
428
+ if (parsed.json) {
429
+ console.log(JSON.stringify(data, null, 2));
430
+ return;
431
+ }
432
+ printToolsDoctorReport(data);
433
+ }
434
+
435
+ async function buildToolsDoctorReport(targetDir = process.cwd()) {
436
+ const dir = path.resolve(targetDir || process.cwd());
437
+ const codexHome = path.join(os.homedir(), '.codex');
438
+ const pluginCache = path.join(codexHome, 'plugins', 'cache');
439
+ const globalHook = await readJsonOrDefault(path.join(codexHome, 'hooks.json'), {});
440
+ const tmux = await findExecutable('tmux');
441
+ const packageInfo = await projectPackageInfo(dir);
442
+ const pack = await findProjectPack(dir);
443
+ const detection = await detectProject(dir, { quiet: true });
444
+ const instructionSurfaces = await findInstructionSurfaces(dir);
445
+ const safety = detectDbSafetyText([
446
+ packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.scripts || {}) : '',
447
+ packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.dependencies || {}) : '',
448
+ packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.devDependencies || {}) : ''
449
+ ].join('\n'));
450
+ const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
451
+ const sqlScan = await scanSqlFiles(dir);
452
+ const rows = [
453
+ readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
454
+ readinessRow('Yam skills', await status({ quiet: true }) === 0 ? 'ready' : 'missing', DEST),
455
+ readinessRow('yam-lite hook', hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled', 'optional UserPromptSubmit guide'),
456
+ readinessRow('tmux', tmux ? 'ready' : 'missing', tmux || 'not found on PATH or common Homebrew paths'),
457
+ readinessRow('Browser plugin', await pluginCacheHas('openai-bundled/browser') ? 'ready' : 'unknown', 'Codex in-app browser cache'),
458
+ readinessRow('Chrome plugin', await pluginCacheHas('openai-bundled/chrome') ? 'ready' : 'unknown', 'Chrome/profile-dependent browser cache'),
459
+ readinessRow('Computer Use', await pluginCacheHas('openai-bundled/computer-use') ? 'ready' : 'unknown', 'desktop UI automation cache'),
460
+ readinessRow('Context7', await context7CacheDetected(pluginCache) ? 'ready' : 'unknown', 'CLI cannot prove deferred Context7 tools; confirm with tool discovery when needed'),
461
+ readinessRow('Supabase plugin', await pluginCacheHas('openai-curated/supabase') ? 'ready' : 'unknown', 'plugin cache only; no DB query performed'),
462
+ readinessRow('Vercel plugin', await pluginCacheHas('openai-curated/vercel') ? 'ready' : 'unknown', 'plugin cache only; no deployment/API call performed')
463
+ ];
464
+ const riskNotes = [
465
+ ...instructionSurfaces.issues.map((message) => ({ level: 'issue', reason: message })),
466
+ ...instructionSurfaces.warnings.map((message) => ({ level: 'warning', reason: message })),
467
+ ...safety.hits,
468
+ ...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
469
+ ];
470
+ return {
471
+ schemaVersion: 1,
472
+ generatedAt: new Date().toISOString(),
473
+ project: dir,
474
+ projectPack: pack || null,
475
+ readiness: rows,
476
+ commands: detection.packageJson ? detection.commands : {},
477
+ packageManager: detection.packageJson ? detection.packageManager : null,
478
+ instructionSurfaces,
479
+ projectSurfaces,
480
+ sqlScan,
481
+ riskNotes,
482
+ routeRecommendations: {
483
+ dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
484
+ currentDocs: 'use current-docs proof only when version/freshness matters',
485
+ ueye: 'use $ueye with real screenshot/browser evidence when feasible',
486
+ mission: 'use $mission only with real subagents/team lanes'
487
+ }
488
+ };
489
+ }
490
+
491
+ function printToolsDoctorReport(data) {
492
+ console.log('yam tools doctor');
493
+ console.log(`Project: ${data.project}`);
494
+ console.log(`Project pack: ${data.projectPack || 'missing'}`);
495
+ console.log('');
496
+ console.log('Readiness:');
497
+ for (const row of data.readiness) console.log(`- ${row.name}: ${row.status} (${row.note})`);
498
+
499
+ if (Object.keys(data.commands).length) {
500
+ console.log('');
501
+ console.log('Detected project commands:');
502
+ for (const [key, value] of Object.entries(data.commands)) {
503
+ console.log(`- ${key}: ${value || '(not found)'}`);
504
+ }
505
+ }
506
+
507
+ if (data.instructionSurfaces.found.length || data.projectSurfaces.length) {
508
+ console.log('');
509
+ console.log('Project surfaces:');
510
+ for (const found of data.instructionSurfaces.found) console.log(`- instruction: ${found}`);
511
+ for (const surface of data.projectSurfaces) console.log(`- ${surface}`);
512
+ }
513
+
514
+ if (data.sqlScan.filesScanned > 0) {
515
+ console.log('');
516
+ console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
517
+ for (const finding of data.sqlScan.findings) {
518
+ console.log(`- ${finding.level}: ${finding.file}: ${finding.reason}`);
519
+ }
520
+ }
521
+
522
+ if (data.riskNotes.length) {
523
+ console.log('');
524
+ console.log('Risk notes:');
525
+ for (const note of data.riskNotes) console.log(`- ${note.level}: ${note.reason}`);
526
+ }
527
+
528
+ console.log('');
529
+ console.log('Route recommendations:');
530
+ console.log(`- DB/Supabase destructive or production write work: ${data.routeRecommendations.dbSupabase}`);
531
+ console.log(`- Current SDK/API/cloud-service behavior: ${data.routeRecommendations.currentDocs}.`);
532
+ console.log(`- UI visual claims: ${data.routeRecommendations.ueye}.`);
533
+ console.log(`- Team execution: ${data.routeRecommendations.mission}.`);
534
+ }
535
+
536
+ function readinessRow(name, status, note) {
537
+ return { name, status, note };
538
+ }
539
+
540
+ async function pluginCacheHas(relative) {
541
+ return exists(path.join(os.homedir(), '.codex', 'plugins', 'cache', relative));
542
+ }
543
+
544
+ async function context7CacheDetected(pluginCache) {
545
+ const candidates = [
546
+ path.join(pluginCache, 'context7'),
547
+ path.join(pluginCache, 'openai-curated', 'context7'),
548
+ path.join(pluginCache, 'modelcontextprotocol', 'context7')
549
+ ];
550
+ for (const candidate of candidates) {
551
+ if (await exists(candidate)) return true;
552
+ }
553
+ return false;
554
+ }
555
+
556
+ async function findExecutable(name) {
557
+ const pathEntries = String(process.env.PATH || '').split(path.delimiter).filter(Boolean);
558
+ const common = name === 'tmux' ? [
559
+ path.join(os.homedir(), '.homebrew', 'bin'),
560
+ '/opt/homebrew/bin',
561
+ '/usr/local/bin',
562
+ '/usr/bin'
563
+ ] : [];
564
+ for (const dir of [...pathEntries, ...common]) {
565
+ const candidate = path.join(dir, name);
566
+ try {
567
+ await fsp.access(candidate, fs.constants.X_OK);
568
+ return candidate;
569
+ } catch {
570
+ // keep scanning
571
+ }
572
+ }
573
+ return '';
574
+ }
575
+
576
+ async function projectPackageInfo(dir) {
577
+ const packageJsonPath = path.join(dir, 'package.json');
578
+ if (!await exists(packageJsonPath)) return { packageJson: false, pkg: null };
579
+ return { packageJson: true, pkg: await readJson(packageJsonPath) };
580
+ }
581
+
582
+ async function detectProjectToolSurfaces(dir, pkg = null) {
583
+ const surfaces = [];
584
+ const deps = {
585
+ ...pkg?.dependencies,
586
+ ...pkg?.devDependencies
587
+ };
588
+ const depNames = Object.keys(deps || {});
589
+ if (depNames.some((name) => name.includes('supabase'))) surfaces.push('Supabase dependency detected');
590
+ if (depNames.some((name) => name.includes('vercel') || name === 'next')) surfaces.push('Vercel/Next-related dependency detected');
591
+ if (depNames.some((name) => /openai|ai$|ai-sdk|@ai-sdk/.test(name))) surfaces.push('AI SDK/API dependency detected');
592
+ if (await exists(path.join(dir, 'supabase'))) surfaces.push('supabase/ directory detected');
593
+ if (await exists(path.join(dir, 'prisma'))) surfaces.push('prisma/ directory detected');
594
+ if (await exists(path.join(dir, 'migrations'))) surfaces.push('migrations/ directory detected');
595
+ if (await exists(path.join(dir, 'vercel.json'))) surfaces.push('vercel.json detected');
596
+ if (await exists(path.join(dir, '.env')) || await exists(path.join(dir, '.env.local'))) surfaces.push('.env file detected; values were not read');
597
+ return surfaces;
598
+ }
599
+
600
+ async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
601
+ const root = path.resolve(rootDir || process.cwd());
602
+ const files = [];
603
+ await collectSqlFiles(root, root, files, { maxFiles, maxDepth, depth: 0 });
604
+ const findings = [];
605
+ for (const file of files) {
606
+ let text = '';
607
+ try {
608
+ const handle = await fsp.open(file, 'r');
609
+ try {
610
+ const buffer = Buffer.alloc(maxBytes);
611
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
612
+ text = buffer.subarray(0, bytesRead).toString('utf8');
613
+ } finally {
614
+ await handle.close();
615
+ }
616
+ } catch (error) {
617
+ findings.push({ file: path.relative(root, file), level: 'warning', reason: `could not read SQL file: ${error.message}` });
618
+ continue;
619
+ }
620
+ const safety = detectDbSafetyText(text);
621
+ for (const hit of safety.hits) {
622
+ findings.push({ file: path.relative(root, file), level: hit.level, reason: hit.reason });
623
+ }
624
+ }
625
+ return {
626
+ filesScanned: files.length,
627
+ maxFiles,
628
+ maxBytes,
629
+ findings
630
+ };
631
+ }
632
+
633
+ async function collectSqlFiles(root, dir, files, options) {
634
+ if (files.length >= options.maxFiles || options.depth > options.maxDepth) return;
635
+ let entries = [];
636
+ try {
637
+ entries = await fsp.readdir(dir, { withFileTypes: true });
638
+ } catch {
639
+ return;
640
+ }
641
+ for (const entry of entries) {
642
+ if (files.length >= options.maxFiles) return;
643
+ const absolute = path.join(dir, entry.name);
644
+ const relative = path.relative(root, absolute);
645
+ if (shouldSkipScanPath(relative, entry.name)) continue;
646
+ if (entry.isDirectory()) {
647
+ await collectSqlFiles(root, absolute, files, { ...options, depth: options.depth + 1 });
648
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.sql')) {
649
+ files.push(absolute);
650
+ }
651
+ }
652
+ }
653
+
654
+ function shouldSkipScanPath(relative, name) {
655
+ const skipNames = new Set(['.git', 'node_modules', '.next', 'dist', 'build', '.turbo', '.vercel', 'coverage']);
656
+ if (skipNames.has(name)) return true;
657
+ return relative.split(path.sep).some((part) => skipNames.has(part));
658
+ }
659
+
660
+ async function hook(args = []) {
661
+ const subcommand = args[0] || 'status';
662
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return hookUsage();
663
+ if (subcommand === 'status') return hookStatus(args.slice(1));
664
+ if (subcommand === 'enable') return hookEnable(args.slice(1));
665
+ if (subcommand === 'disable') return hookDisable(args.slice(1));
666
+ if (subcommand === 'run') return hookRun(args.slice(1));
667
+ console.error(`unknown hook command: ${subcommand}`);
668
+ return hookUsage();
669
+ }
670
+
671
+ function hookUsage() {
672
+ console.log(`yam hook
673
+
674
+ Usage:
675
+ yam hook status [--global|--project dir]
676
+ yam hook enable lite [--global|--project dir]
677
+ yam hook disable [lite] [--global|--project dir]
678
+ yam hook run lite
679
+
680
+ Notes:
681
+ yam-lite is opt-in and advisory-only. It does not run checks, tmux, subagents, or proof gates.
682
+ `);
683
+ }
684
+
685
+ function parseHookArgs(args = []) {
686
+ const result = { mode: 'project', projectDir: process.cwd(), profile: 'lite' };
687
+ for (let index = 0; index < args.length; index += 1) {
688
+ const arg = args[index];
689
+ if (arg === 'lite') {
690
+ result.profile = 'lite';
691
+ } else if (arg === '--global') {
692
+ result.mode = 'global';
693
+ } else if (arg === '--project') {
694
+ result.mode = 'project';
695
+ result.projectDir = args[index + 1] || process.cwd();
696
+ index += 1;
697
+ } else if (arg.startsWith('--project=')) {
698
+ result.mode = 'project';
699
+ result.projectDir = arg.slice('--project='.length) || process.cwd();
700
+ }
701
+ }
702
+ return result;
703
+ }
704
+
705
+ function hookPathFor(parsed) {
706
+ if (parsed.mode === 'global') return path.join(os.homedir(), '.codex', 'hooks.json');
707
+ return path.join(path.resolve(parsed.projectDir || process.cwd()), '.codex', 'hooks.json');
708
+ }
709
+
710
+ function isYamLiteHook(handler = {}) {
711
+ return handler?.type === 'command' && String(handler.command || '').includes('yam.js hook run lite');
712
+ }
713
+
714
+ function stripYamLiteHooks(config = {}) {
715
+ const next = { ...config };
716
+ for (const event of Object.keys(next)) {
717
+ const entries = Array.isArray(next[event]) ? next[event] : [];
718
+ const keptEntries = [];
719
+ for (const entry of entries) {
720
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamLiteHook(handler)) : [];
721
+ const rest = { ...entry, hooks };
722
+ if (hooks.length > 0) keptEntries.push(rest);
723
+ }
724
+ if (keptEntries.length > 0) next[event] = keptEntries;
725
+ else delete next[event];
726
+ }
727
+ return next;
728
+ }
729
+
730
+ function withYamLiteHook(config = {}) {
731
+ const next = stripYamLiteHooks(config);
732
+ const event = 'UserPromptSubmit';
733
+ const entry = {
734
+ hooks: [
735
+ {
736
+ type: 'command',
737
+ command: YAM_LITE_HOOK_COMMAND,
738
+ timeout: 5
739
+ }
740
+ ]
741
+ };
742
+ next[event] = [...(Array.isArray(next[event]) ? next[event] : []), entry];
743
+ return next;
744
+ }
745
+
746
+ async function hookStatus(args = []) {
747
+ const parsed = parseHookArgs(args);
748
+ const target = hookPathFor(parsed);
749
+ const config = await readJsonOrDefault(target, {});
750
+ const enabled = hookConfigHasYamLite(config);
751
+ console.log(`yam-lite hook: ${enabled ? 'enabled' : 'disabled'}`);
752
+ console.log(`scope: ${parsed.mode}`);
753
+ console.log(`file: ${target}`);
754
+ }
755
+
756
+ function hookConfigHasYamLite(config = {}) {
757
+ return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
758
+ return Array.isArray(entry?.hooks) && entry.hooks.some(isYamLiteHook);
759
+ }));
760
+ }
761
+
762
+ async function hookEnable(args = []) {
763
+ const parsed = parseHookArgs(args);
764
+ if (parsed.profile !== 'lite') {
765
+ console.error('Only yam-lite hook is supported: yam hook enable lite');
766
+ process.exitCode = 1;
767
+ return;
768
+ }
769
+ const target = hookPathFor(parsed);
770
+ const current = await readJsonOrDefault(target, {});
771
+ const next = withYamLiteHook(current);
772
+ await fsp.mkdir(path.dirname(target), { recursive: true });
773
+ if (await exists(target)) {
774
+ const backup = `${target}.yam-backup-${timestampId()}`;
775
+ await fsp.copyFile(target, backup);
776
+ console.log(`backup: ${backup}`);
777
+ }
778
+ await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
779
+ console.log(`yam-lite hook enabled (${parsed.mode}): ${target}`);
780
+ console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
781
+ }
782
+
783
+ async function hookDisable(args = []) {
784
+ const parsed = parseHookArgs(args);
785
+ const target = hookPathFor(parsed);
786
+ const current = await readJsonOrDefault(target, {});
787
+ if (!hookConfigHasYamLite(current)) {
788
+ console.log(`yam-lite hook already disabled (${parsed.mode}): ${target}`);
789
+ return;
790
+ }
791
+ const next = stripYamLiteHooks(current);
792
+ if (Object.keys(next).length === 0) {
793
+ await rmrf(target);
794
+ } else {
795
+ await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
796
+ }
797
+ console.log(`yam-lite hook disabled (${parsed.mode}): ${target}`);
798
+ }
799
+
800
+ async function hookRun(args = []) {
801
+ const profile = args[0] || 'lite';
802
+ if (profile !== 'lite') {
803
+ console.log(JSON.stringify({ continue: true }));
804
+ return;
805
+ }
806
+ const input = await readStdinJson();
807
+ const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
808
+ const cwd = String(input?.cwd || process.cwd());
809
+ const prompt = extractPrompt(input);
810
+ const additionalContext = await buildYamLiteContext({ cwd, prompt });
811
+ const output = {
812
+ continue: true,
813
+ hookSpecificOutput: {
814
+ hookEventName: event,
815
+ additionalContext
816
+ }
817
+ };
818
+ console.log(JSON.stringify(output));
819
+ }
820
+
821
+ async function readStdinJson() {
822
+ const chunks = [];
823
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
824
+ const text = Buffer.concat(chunks).toString('utf8').trim();
825
+ if (!text) return {};
826
+ try {
827
+ return JSON.parse(text);
828
+ } catch {
829
+ return {};
830
+ }
831
+ }
832
+
833
+ function extractPrompt(input = {}) {
834
+ const candidates = [
835
+ input.prompt,
836
+ input.user_prompt,
837
+ input.userPrompt,
838
+ input.message,
839
+ input.text,
840
+ input.input
841
+ ];
842
+ const found = candidates.find((value) => typeof value === 'string' && value.trim());
843
+ return found ? found.trim() : '';
844
+ }
845
+
846
+ async function buildYamLiteContext({ cwd, prompt }) {
847
+ const lines = [
848
+ 'yam-lite guide active: keep project direction visible, preserve momentum, and deepen when scope, risk, or user intent calls for it.',
849
+ 'Default: do a basic direction-fit and honest-verification check; avoid broad reading for small work and never claim proof without evidence.'
850
+ ];
851
+ const pack = await findProjectPack(cwd).catch(() => null);
852
+ if (pack) lines.push(`Project direction: prefer ${path.basename(pack)} before broad exploration.`);
853
+ const memorySummary = path.join(path.resolve(cwd), '.yam', 'memory', 'summary.md');
854
+ if (await exists(memorySummary)) lines.push('Project memory: prefer .yam/memory/summary.md over reading every record.');
855
+ const safetyHint = yamLiteSafetyHint(prompt);
856
+ if (safetyHint) lines.push(safetyHint);
857
+ const docsHint = yamLiteCurrentDocsHint(prompt);
858
+ if (docsHint) lines.push(docsHint);
859
+ const routeHint = yamLiteRouteHint(prompt);
860
+ if (routeHint) lines.push(routeHint);
861
+ if (await exists(path.join(path.resolve(cwd), '.sneakoscope'))) {
862
+ lines.push('Caution: active .sneakoscope detected; avoid mixing proof gates unless the user explicitly wants it.');
863
+ }
864
+ return lines.join('\n');
865
+ }
866
+
867
+ function yamLiteRouteHint(prompt = '') {
868
+ const text = String(prompt || '').toLowerCase();
869
+ if (!text) return '';
870
+ if (/\$(quick|ueye|question|scout|deep|mission)\b/.test(text)) return '';
871
+ if (/(subagent|team|mission|팀\s*단위|에이전트.*팀)/i.test(prompt)) return 'Route hint: use $mission only for real subagent/team execution; otherwise use $deep for heavy single-agent verification.';
872
+ if (/(auth|payment|billing|permission|security|deploy|release|migration|database|supabase|vercel|db|보안|결제|배포|마이그레이션|데이터베이스|권한)/i.test(prompt)) return 'Route hint: risky surface detected; prefer $deep unless the user asks for real team/subagent execution.';
873
+ if (/(screenshot|reference image|ui|ux|design|visual|화면|스크린샷|레퍼런스|디자인)/i.test(prompt)) return 'Route hint: visual/design work should use $ueye when screenshot, URL, reference image, or visual evidence matters.';
874
+ if (/(\?|무엇|뭐지|설명|가능|how|what|why|can i)/i.test(prompt)) return 'Route hint: direct conceptual questions can use $question; comparisons or current sources can use $scout.';
875
+ return 'Route hint: small scoped code changes can use $quick; broaden only if verification or risk requires it.';
876
+ }
877
+
878
+ function yamLiteSafetyHint(prompt = '') {
879
+ const safety = detectDbSafetyText(prompt);
880
+ if (!safety.hits.length) return '';
881
+ return 'Safety hint: destructive DB/Supabase or production-write signal detected; prefer $deep, require explicit approval, and do not claim safe without evidence.';
882
+ }
883
+
884
+ function yamLiteCurrentDocsHint(prompt = '') {
885
+ if (!needsCurrentDocsProof(prompt)) return '';
886
+ return 'Current-docs hint: modern SDK/API/cloud-service behavior appears version-sensitive; use official/Context7 docs proof before relying on memory.';
887
+ }
888
+
889
+ function needsCurrentDocsProof(text = '') {
890
+ const value = String(text || '');
891
+ const docsSensitive = /(sdk|api|library|framework|package|dependency|next\.?js|react|supabase|vercel|openai|ai sdk|stripe|prisma|drizzle|tailwind|shadcn|auth|deploy|migration|upgrade|라이브러리|프레임워크|패키지|의존성|버전|마이그레이션|배포)/i;
892
+ const freshness = /(latest|current|recent|new|upgrade|migrate|deprecated|breaking|docs?|official|v\d+|202[5-9]|최신|최근|업데이트|공식|문서|버전|변경|마이그레이션)/i;
893
+ return docsSensitive.test(value) && freshness.test(value);
894
+ }
895
+
896
+ async function safety(args = []) {
897
+ const positionals = args[0] === 'scan' ? args.slice(1) : args;
898
+ const inlineText = positionals.join(' ').trim();
899
+ const stdin = await readStdinTextIfAvailable();
900
+ const text = [inlineText, stdin].filter(Boolean).join('\n').trim();
901
+ if (!text) {
902
+ console.log('DB/Supabase safety lite');
903
+ console.log('Status: no input');
904
+ console.log('Usage: yam safety "supabase db reset"');
905
+ return;
906
+ }
907
+ const result = detectDbSafetyText(text);
908
+ printSafetyResult(result);
909
+ }
910
+
911
+ async function readStdinTextIfAvailable() {
912
+ if (process.stdin.isTTY) return '';
913
+ const chunks = [];
914
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
915
+ return Buffer.concat(chunks).toString('utf8').trim();
916
+ }
917
+
918
+ function detectDbSafetyText(text = '') {
919
+ const value = String(text || '');
920
+ const hits = [];
921
+ const checks = [
922
+ ['danger', /\bdrop\s+(table|schema|database|view|index)\b/i, 'DROP statement can destroy database objects'],
923
+ ['danger', /\btruncate\s+(table\s+)?[a-z0-9_".]+/i, 'TRUNCATE statement can destroy table data'],
924
+ ['danger', /\bdelete\s+from\s+[a-z0-9_".]+/i, 'DELETE FROM can destroy row data'],
925
+ ['danger', /\bupdate\s+[a-z0-9_".]+\s+set\b/i, 'UPDATE can mutate row data'],
926
+ ['warning', /\balter\s+table\s+[a-z0-9_".]+/i, 'ALTER TABLE can change schema or lock production data'],
927
+ ['danger', /\bsupabase\s+db\s+(reset|push)\b/i, 'supabase db reset/push can mutate database state'],
928
+ ['warning', /\bsupabase\s+(migration|migrations|db\s+diff|db\s+pull)\b/i, 'Supabase migration/db command needs explicit environment awareness'],
929
+ ['danger', /\b(prisma\s+migrate\s+(deploy|reset|dev)|drizzle-kit\s+(push|drop|migrate)|knex\s+migrate|sequelize\s+db:migrate)\b/i, 'ORM migration command can mutate schema/data'],
930
+ ['danger', /\bpsql\b[\s\S]{0,160}\b(drop|truncate|delete\s+from|update\s+[a-z0-9_".]+\s+set|alter\s+table)\b/i, 'psql command contains destructive SQL'],
931
+ ['warning', /\b(create\s+policy|alter\s+policy|drop\s+policy|grant\s+|revoke\s+)/i, 'RLS/policy/permission change affects data access safety']
932
+ ];
933
+ for (const [level, pattern, reason] of checks) {
934
+ if (pattern.test(value)) hits.push({ level, reason });
935
+ }
936
+ const productionSignal = /\b(prod|production|live|remote|linked|service[_-]?role|database_url|--db-url|--linked|--remote|--project-ref)\b/i.test(value);
937
+ if (productionSignal) hits.push({ level: 'warning', reason: 'production/remote credential or environment signal detected' });
938
+ const unique = [];
939
+ const seen = new Set();
940
+ for (const hit of hits) {
941
+ const key = `${hit.level}:${hit.reason}`;
942
+ if (seen.has(key)) continue;
943
+ seen.add(key);
944
+ unique.push(hit);
945
+ }
946
+ return {
947
+ hits: unique,
948
+ recommendation: unique.length ? '$deep' : '$quick or current route',
949
+ truth: 'assumed'
950
+ };
951
+ }
952
+
953
+ function printSafetyResult(result) {
954
+ console.log('DB/Supabase safety lite');
955
+ console.log(`Status: ${result.hits.length ? 'risk detected' : 'no destructive DB pattern detected'}`);
956
+ console.log(`Recommended route: ${result.recommendation}`);
957
+ console.log(`Truth status: ${result.truth} (text-pattern scan only)`);
958
+ if (result.hits.length) {
959
+ console.log('');
960
+ console.log('Hits:');
961
+ for (const hit of result.hits) console.log(`- ${hit.level}: ${hit.reason}`);
962
+ console.log('');
963
+ console.log('Guardrail: require explicit user approval, confirm target environment, prefer read-only inspection first, and avoid claiming safe without evidence.');
964
+ }
965
+ }
966
+
967
+ async function proof(args = []) {
968
+ if (args[0] === 'write') return proofWrite(args.slice(1));
969
+ const parsed = parseProofArgs(args);
970
+ const artifact = await readProofArtifact(parsed.flags.from, parsed.dir);
971
+ const summary = buildProofSummary(parsed, artifact);
972
+ if (!summary) return;
973
+
974
+ if (parsed.flags.json) {
975
+ console.log(JSON.stringify(summary, null, 2));
976
+ return;
977
+ }
978
+
979
+ console.log('yam proof summary');
980
+ console.log(`- Route: ${summary.route}`);
981
+ console.log(`- Goal: ${summary.goal || '(not supplied)'}`);
982
+ console.log(`- Source: ${summary.source || '(no artifact found; flags/template only)'}`);
983
+ console.log(`- Truth status: ${summary.truth}`);
984
+ printProofList('Commands', summary.commands);
985
+ printProofList('Evidence', summary.evidence);
986
+ printProofList('Visual evidence', summary.visual);
987
+ printProofList('Runtime evidence', summary.runtime);
988
+ printProofList('Changed surfaces', summary.changed);
989
+ printProofList('Skipped', summary.skipped);
990
+ printProofList('Blocked', summary.blocked);
991
+ printProofList('Assumptions', summary.assumptions);
992
+ console.log(`- Cleanup: ${summary.cleanup || '(not supplied)'}`);
993
+ if (!hasProofEvidence(parsed) && !artifact.path) {
994
+ console.log('');
995
+ console.log('Note: no proof artifact or evidence flags were supplied, so this is a proof template, not verification.');
996
+ }
997
+ }
998
+
999
+ async function proofWrite(args = []) {
1000
+ const parsed = parseProofArgs(args);
1001
+ const artifact = parsed.flags.from ? await readProofArtifact(parsed.flags.from, parsed.dir) : emptyProofArtifact();
1002
+ const summary = buildProofSummary(parsed, artifact);
1003
+ if (!summary) return;
1004
+ const format = String(parsed.flags.format || '').toLowerCase();
1005
+ const target = proofWriteTarget(parsed, format);
1006
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1007
+ if (target.endsWith('.md') || format === 'md' || format === 'markdown') {
1008
+ await fsp.writeFile(target, renderProofMarkdown(summary));
1009
+ } else {
1010
+ await fsp.writeFile(target, `${JSON.stringify({
1011
+ schemaVersion: 1,
1012
+ writtenAt: new Date().toISOString(),
1013
+ ...summary
1014
+ }, null, 2)}\n`);
1015
+ }
1016
+ console.log(`proof written: ${target}`);
1017
+ console.log(`truth status: ${summary.truth}`);
1018
+ }
1019
+
1020
+ function proofWriteTarget(parsed, format = '') {
1021
+ const explicit = parsed.flags.out || parsed.flags.file;
1022
+ if (explicit) return path.resolve(expandHome(explicit));
1023
+ const dir = path.resolve(parsed.dir || process.cwd());
1024
+ const extension = format === 'md' || format === 'markdown' ? 'md' : 'json';
1025
+ return path.join(dir, '.yam', `proof.${extension}`);
1026
+ }
1027
+
1028
+ function buildProofSummary(parsed, artifact) {
1029
+ const allowed = new Set(['verified', 'proven', 'partial', 'fixture_only', 'fixture_instrumented_real', 'integration_optional', 'real_required_missing', 'skipped', 'blocked', 'assumed']);
1030
+ const requestedTruth = parsed.flags.truth || '';
1031
+ let truth = requestedTruth || artifact.truth || (hasProofEvidence(parsed) || artifact.path ? 'partial' : 'assumed');
1032
+ if (!allowed.has(truth) && !requestedTruth) truth = 'partial';
1033
+ if (!allowed.has(truth)) {
1034
+ console.error(`invalid truth status: ${truth}`);
1035
+ console.error(`allowed: ${[...allowed].join(', ')}`);
1036
+ process.exitCode = 1;
1037
+ return null;
1038
+ }
1039
+ const secretHit = findSensitivePattern(Object.values(parsed.flags).flat().join('\n'));
1040
+ if (secretHit) {
1041
+ console.error(`proof blocked: possible secret pattern detected (${secretHit})`);
1042
+ process.exitCode = 1;
1043
+ return null;
1044
+ }
1045
+ return {
1046
+ route: parsed.flags.route || artifact.route || 'unspecified',
1047
+ goal: parsed.flags.goal || artifact.goal || '',
1048
+ truth,
1049
+ source: artifact.path || '',
1050
+ commands: [...artifact.commands, ...arrayFlag(parsed.flags.command)],
1051
+ evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1052
+ visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1053
+ runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
1054
+ cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1055
+ changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1056
+ skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
1057
+ blocked: [...artifact.blocked, ...arrayFlag(parsed.flags.blocked)],
1058
+ assumptions: [...artifact.assumptions, ...arrayFlag(parsed.flags.assumed || parsed.flags.assumption)]
1059
+ };
1060
+ }
1061
+
1062
+ function renderProofMarkdown(summary) {
1063
+ const lines = [
1064
+ '# yam Proof',
1065
+ '',
1066
+ `- Route: ${summary.route}`,
1067
+ `- Goal: ${summary.goal || ''}`,
1068
+ `- Truth status: ${summary.truth}`,
1069
+ `- Source: ${summary.source || ''}`,
1070
+ `- Cleanup: ${summary.cleanup || ''}`,
1071
+ '',
1072
+ '## Commands',
1073
+ ...renderProofMarkdownList(summary.commands),
1074
+ '',
1075
+ '## Evidence',
1076
+ ...renderProofMarkdownList(summary.evidence),
1077
+ '',
1078
+ '## Visual evidence',
1079
+ ...renderProofMarkdownList(summary.visual),
1080
+ '',
1081
+ '## Runtime evidence',
1082
+ ...renderProofMarkdownList(summary.runtime),
1083
+ '',
1084
+ '## Changed surfaces',
1085
+ ...renderProofMarkdownList(summary.changed),
1086
+ '',
1087
+ '## Skipped',
1088
+ ...renderProofMarkdownList(summary.skipped),
1089
+ '',
1090
+ '## Blocked',
1091
+ ...renderProofMarkdownList(summary.blocked),
1092
+ '',
1093
+ '## Assumptions',
1094
+ ...renderProofMarkdownList(summary.assumptions),
1095
+ ''
1096
+ ];
1097
+ return lines.join('\n');
1098
+ }
1099
+
1100
+ function renderProofMarkdownList(values) {
1101
+ return values.length ? values.map((value) => `- ${value}`) : ['-'];
1102
+ }
1103
+
1104
+ function parseProofArgs(args = []) {
1105
+ const flags = {};
1106
+ const positionals = [];
1107
+ const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'from', 'format', 'out', 'file', 'json']);
1108
+ for (let index = 0; index < args.length; index += 1) {
1109
+ const arg = args[index];
1110
+ if (arg.startsWith('--')) {
1111
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1112
+ const key = rawKey.slice(2);
1113
+ if (!aliases.has(key)) continue;
1114
+ if (key === 'json') {
1115
+ flags.json = true;
1116
+ continue;
1117
+ }
1118
+ const value = inlineValue ?? args[index + 1] ?? '';
1119
+ if (inlineValue === undefined) index += 1;
1120
+ if (['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked', 'assumed', 'assumption'].includes(key)) {
1121
+ flags[key] = [...arrayFlag(flags[key]), value];
1122
+ } else {
1123
+ flags[key] = value;
1124
+ }
1125
+ continue;
1126
+ }
1127
+ positionals.push(arg);
1128
+ }
1129
+ const dir = positionals.find(looksLikeDirectoryArg) || process.cwd();
1130
+ return { flags, dir };
1131
+ }
1132
+
1133
+ async function readProofArtifact(explicitFile = '', targetDir = process.cwd()) {
1134
+ const empty = emptyProofArtifact();
1135
+ const candidates = explicitFile ? [expandHome(explicitFile)] : [
1136
+ path.join(path.resolve(targetDir), '.yam', 'proof.json'),
1137
+ path.join(path.resolve(targetDir), '.yam', 'proof.md'),
1138
+ path.join(path.resolve(targetDir), '.yam', 'runtime-proof.md')
1139
+ ];
1140
+ for (const candidate of candidates) {
1141
+ if (!await exists(candidate)) continue;
1142
+ if (candidate.endsWith('.json')) return parseProofJson(candidate, empty);
1143
+ return parseProofMarkdown(candidate, empty);
1144
+ }
1145
+ return empty;
1146
+ }
1147
+
1148
+ function emptyProofArtifact() {
1149
+ return {
1150
+ path: '',
1151
+ route: '',
1152
+ goal: '',
1153
+ truth: '',
1154
+ commands: [],
1155
+ evidence: [],
1156
+ visual: [],
1157
+ runtime: [],
1158
+ cleanup: '',
1159
+ changed: [],
1160
+ skipped: [],
1161
+ blocked: [],
1162
+ assumptions: []
1163
+ };
1164
+ }
1165
+
1166
+ async function parseProofJson(file, empty) {
1167
+ try {
1168
+ const data = await readJson(file);
1169
+ return {
1170
+ ...empty,
1171
+ path: file,
1172
+ route: String(data.route || ''),
1173
+ goal: String(data.goal || data.mission || ''),
1174
+ truth: String(data.truth || data.truthStatus || data.status || ''),
1175
+ commands: arrayFlag(data.commands || data.command),
1176
+ evidence: arrayFlag(data.evidence),
1177
+ visual: arrayFlag(data.visual || data.visualEvidence),
1178
+ runtime: arrayFlag(data.runtime || data.runtimeEvidence),
1179
+ cleanup: String(data.cleanup || data.cleanupStatus || ''),
1180
+ changed: arrayFlag(data.changed || data.files),
1181
+ skipped: arrayFlag(data.skipped),
1182
+ blocked: arrayFlag(data.blocked),
1183
+ assumptions: arrayFlag(data.assumptions || data.assumed)
1184
+ };
1185
+ } catch (error) {
1186
+ return { ...empty, path: file, blocked: [`Could not parse JSON proof: ${error.message}`], truth: 'blocked' };
1187
+ }
1188
+ }
1189
+
1190
+ async function parseProofMarkdown(file, empty) {
1191
+ const text = await readText(file);
1192
+ const finalTruth = firstProofList(text, ['Final Truth Status'])[0] || '';
1193
+ return {
1194
+ ...empty,
1195
+ path: file,
1196
+ route: readProofField(text, 'Route'),
1197
+ goal: readProofField(text, 'Goal') || readProofField(text, 'Mission goal'),
1198
+ truth: readProofField(text, 'Truth status') || readProofField(text, 'Final Truth Status') || finalTruth.split(':')[0].trim(),
1199
+ commands: firstProofList(text, ['Commands', 'Verification']),
1200
+ evidence: readProofList(text, 'Evidence'),
1201
+ visual: readProofList(text, 'Visual evidence'),
1202
+ runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
1203
+ cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1204
+ changed: firstProofList(text, ['Changed surfaces', 'Files']),
1205
+ skipped: readProofList(text, 'Skipped'),
1206
+ blocked: readProofList(text, 'Blocked'),
1207
+ assumptions: readProofList(text, 'Assumptions')
1208
+ };
1209
+ }
1210
+
1211
+ function readProofField(text, label) {
1212
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1213
+ const patterns = [
1214
+ new RegExp(`^-\\s*${escaped}:\\s*(.+)$`, 'im'),
1215
+ new RegExp(`^##\\s*${escaped}\\s*\\n\\s*([^\\n]+)`, 'im')
1216
+ ];
1217
+ for (const pattern of patterns) {
1218
+ const match = text.match(pattern);
1219
+ if (match) return match[1].trim();
1220
+ }
1221
+ return '';
1222
+ }
1223
+
1224
+ function readProofList(text, label) {
1225
+ const section = getMarkdownSection(text, label);
1226
+ if (!section) {
1227
+ const field = readProofField(text, label);
1228
+ return field ? [field] : [];
1229
+ }
1230
+ return section.split(/\r?\n/)
1231
+ .map((line) => line.match(/^\s*-\s+(.+)$/)?.[1]?.trim())
1232
+ .filter(Boolean);
1233
+ }
1234
+
1235
+ function firstProofList(text, labels = []) {
1236
+ for (const label of labels) {
1237
+ const values = readProofList(text, label);
1238
+ if (values.length) return values;
1239
+ }
1240
+ return [];
1241
+ }
1242
+
1243
+ function arrayFlag(value) {
1244
+ if (!value) return [];
1245
+ return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean);
1246
+ }
1247
+
1248
+ function hasProofEvidence(parsed) {
1249
+ const flags = parsed.flags || {};
1250
+ return ['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1251
+ }
1252
+
1253
+ function printProofList(label, values) {
1254
+ console.log(`- ${label}:`);
1255
+ if (!values.length) {
1256
+ console.log(' - (none supplied)');
1257
+ return;
1258
+ }
1259
+ for (const value of values) console.log(` - ${value}`);
1260
+ }
1261
+
1262
+ function expandHome(value = '') {
1263
+ return String(value || '').replace(/^~(?=$|\/)/, os.homedir());
1264
+ }
1265
+
1266
+ async function examples() {
1267
+ console.log(await readText(path.join(ROOT, 'COMMANDS.md')));
1268
+ }
1269
+
1270
+ async function initProject(targetDir = process.cwd()) {
1271
+ const resolved = path.resolve(targetDir);
1272
+ const target = path.join(resolved, PROJECT_PACK);
1273
+ const template = path.join(ROOT, 'templates', PROJECT_PACK);
1274
+ if (!await exists(template)) throw new Error(`missing template: ${template}`);
1275
+ await fsp.mkdir(resolved, { recursive: true });
1276
+ const existingPack = await findProjectPack(resolved);
1277
+ if (existingPack) {
1278
+ console.log(`${path.basename(existingPack)} already exists: ${existingPack}`);
1279
+ return;
1280
+ }
1281
+ await fsp.copyFile(template, target);
1282
+ await maybeAppendDetectedCommands(target, resolved);
1283
+ console.log(`created ${target}`);
1284
+ }
1285
+
1286
+ async function inspectProjectPack(targetDir = process.cwd()) {
1287
+ const resolved = path.resolve(targetDir || process.cwd());
1288
+ const target = await findProjectPack(resolved);
1289
+ const issues = [];
1290
+ const warnings = [];
1291
+
1292
+ console.log(`Project: ${resolved}`);
1293
+ if (!target) {
1294
+ console.log(`Pack: missing ${path.join(resolved, PROJECT_PACK)}`);
1295
+ console.log(`Create it with: yam init-project ${resolved}`);
1296
+ process.exitCode = 1;
1297
+ return;
1298
+ }
1299
+
1300
+ const text = await readText(target);
1301
+ const stat = await fsp.stat(target);
1302
+ const words = countWords(text);
1303
+ const lines = text.split(/\r?\n/).length;
1304
+ const missingSections = REQUIRED_PACK_SECTIONS.filter((section) => !hasHeading(text, section));
1305
+ const placeholderLines = text.split(/\r?\n/).filter((line) => /^\s*-\s+[^:]+:\s*$/.test(line)).length;
1306
+ const detection = await detectProject(resolved, { quiet: true });
1307
+ const packAgeDays = Math.floor((Date.now() - stat.mtimeMs) / 86400000);
1308
+ const instructionSurfaces = await findInstructionSurfaces(resolved);
1309
+
1310
+ if (missingSections.length) issues.push(`missing section(s): ${missingSections.join(', ')}`);
1311
+ if (words > 1200) warnings.push(`pack is long (${words} words); keep the Karpathy-style core compact`);
1312
+ if (words < 80) warnings.push(`pack is very short (${words} words); direction may be too thin to reuse`);
1313
+ if (packAgeDays > PACK_STALE_DAYS) warnings.push(`pack is ${packAgeDays} days old; review whether direction or commands changed`);
1314
+ if (placeholderLines > 12) warnings.push(`${placeholderLines} placeholder lines are still blank`);
1315
+ warnings.push(...commandDriftWarnings(text, detection));
1316
+ issues.push(...instructionSurfaces.issues);
1317
+ warnings.push(...instructionSurfaces.warnings);
1318
+ if (path.basename(target) === LEGACY_PROJECT_PACK) {
1319
+ warnings.push(`using legacy project pack ${LEGACY_PROJECT_PACK}; rename to ${PROJECT_PACK} when convenient`);
1320
+ }
1321
+
1322
+ console.log(`Pack: ${target}`);
1323
+ console.log(`Size: ${words} words, ${lines} lines`);
1324
+ console.log(`Age: ${packAgeDays} day(s), modified ${formatDate(stat.mtime)}`);
1325
+ console.log(`Required sections: ${missingSections.length ? 'missing some' : 'ok'}`);
1326
+ console.log(`Blank placeholders: ${placeholderLines}`);
1327
+ console.log(`Instruction surfaces: ${instructionSurfaces.found.length ? instructionSurfaces.found.join(', ') : 'none detected'}`);
1328
+
1329
+ if (detection.packageJson) {
1330
+ console.log('');
1331
+ console.log('Detected commands to keep in the pack:');
1332
+ for (const [key, value] of Object.entries(detection.commands)) {
1333
+ console.log(`- ${key}: ${value || '(not found)'}`);
1334
+ }
1335
+ }
1336
+
1337
+ if (issues.length || warnings.length) {
1338
+ console.log('');
1339
+ console.log('Pack notes:');
1340
+ for (const issue of issues) console.log(`- issue: ${issue}`);
1341
+ for (const warning of warnings) console.log(`- warning: ${warning}`);
1342
+ } else {
1343
+ console.log('');
1344
+ console.log('yam pack: ok');
1345
+ }
1346
+
1347
+ if (issues.length) process.exitCode = 1;
1348
+ }
1349
+
1350
+ async function findInstructionSurfaces(dir) {
1351
+ const candidates = [
1352
+ { path: 'AGENTS.md', level: 'warning', note: 'active AGENTS.md may override route behavior; make sure it does not conflict with yam' },
1353
+ { path: 'CLAUDE.md', level: 'warning', note: 'active CLAUDE.md may carry non-yam instructions' },
1354
+ { path: 'RULES.md', level: 'warning', note: 'active RULES.md may carry non-yam instructions' },
1355
+ { path: '.codex/AGENTS.md', level: 'warning', note: 'active .codex/AGENTS.md may override project behavior' },
1356
+ { path: '.codex/SNEAKOSCOPE.md', level: 'issue', note: 'active Sneakoscope instruction file detected' },
1357
+ { path: '.codex/hooks.json', level: 'issue', note: 'active Codex hook file detected' },
1358
+ { path: '.sneakoscope', level: 'issue', note: 'active Sneakoscope directory detected' },
1359
+ { path: '.agents', level: 'warning', note: 'project-local .agents directory may add additional skills or instructions' }
1360
+ ];
1361
+ const found = [];
1362
+ const issues = [];
1363
+ const warnings = [];
1364
+
1365
+ for (const candidate of candidates) {
1366
+ const absolute = path.join(dir, candidate.path);
1367
+ if (!await exists(absolute)) continue;
1368
+ found.push(candidate.path);
1369
+ const message = `${candidate.path}: ${candidate.note}`;
1370
+ if (candidate.level === 'issue') issues.push(message);
1371
+ else warnings.push(message);
1372
+ }
1373
+
1374
+ return { found, issues, warnings };
1375
+ }
1376
+
1377
+ function commandDriftWarnings(text, detection) {
1378
+ if (!detection.packageJson) return [];
1379
+ const warnings = [];
1380
+ const section = getMarkdownSection(text, 'Detected Commands');
1381
+ if (!section) {
1382
+ warnings.push('package.json scripts detected, but no Detected Commands section is recorded');
1383
+ return warnings;
1384
+ }
1385
+
1386
+ const labels = {
1387
+ dev: 'Dev',
1388
+ typecheck: 'Typecheck',
1389
+ lint: 'Lint',
1390
+ test: 'Test',
1391
+ build: 'Build'
1392
+ };
1393
+ for (const [key, label] of Object.entries(labels)) {
1394
+ const detected = detection.commands[key] || '';
1395
+ const recorded = readBulletValue(section, label);
1396
+ if (detected && !recorded) warnings.push(`Detected Commands missing ${label}: expected ${detected}`);
1397
+ if (detected && recorded && recorded !== detected) {
1398
+ warnings.push(`Detected Commands stale for ${label}: recorded "${recorded}", detected "${detected}"`);
1399
+ }
1400
+ if (!detected && recorded) warnings.push(`Detected Commands has ${label}="${recorded}", but no matching package script was detected`);
1401
+ }
1402
+ return warnings;
1403
+ }
1404
+
1405
+ function getMarkdownSection(text, heading) {
1406
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1407
+ const match = text.match(new RegExp(`^##\\s+${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, 'm'));
1408
+ return match ? match[1] : '';
1409
+ }
1410
+
1411
+ function readBulletValue(section, label) {
1412
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1413
+ const pattern = new RegExp(`^-\\s*${escaped}:\\s*(.*)$`, 'i');
1414
+ for (const line of section.split(/\r?\n/)) {
1415
+ const match = line.match(pattern);
1416
+ if (match) return match[1].trim().replace(/^`|`$/g, '');
1417
+ }
1418
+ return '';
1419
+ }
1420
+
1421
+ function formatDate(date) {
1422
+ return date.toISOString().slice(0, 10);
1423
+ }
1424
+
1425
+ async function findProjectPack(dir) {
1426
+ const primary = path.join(dir, PROJECT_PACK);
1427
+ if (await exists(primary)) return primary;
1428
+ const legacy = path.join(dir, LEGACY_PROJECT_PACK);
1429
+ if (await exists(legacy)) return legacy;
1430
+ return null;
1431
+ }
1432
+
1433
+ function hasHeading(text, heading) {
1434
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1435
+ return new RegExp(`^#{2,3}\\s+${escaped}\\s*$`, 'm').test(text);
1436
+ }
1437
+
1438
+ function countWords(text) {
1439
+ return text.trim().split(/\s+/).filter(Boolean).length;
1440
+ }
1441
+
1442
+ async function maybeAppendDetectedCommands(target, dir) {
1443
+ const detection = await detectProject(dir, { quiet: true });
1444
+ if (!detection.packageJson) return;
1445
+ const lines = [
1446
+ '',
1447
+ '<!-- yam detected package scripts -->',
1448
+ '',
1449
+ '## Detected Commands',
1450
+ '',
1451
+ `- Package manager: ${detection.packageManager}`,
1452
+ `- Dev: ${detection.commands.dev || ''}`,
1453
+ `- Typecheck: ${detection.commands.typecheck || ''}`,
1454
+ `- Lint: ${detection.commands.lint || ''}`,
1455
+ `- Test: ${detection.commands.test || ''}`,
1456
+ `- Build: ${detection.commands.build || ''}`,
1457
+ ''
1458
+ ];
1459
+ await fsp.appendFile(target, lines.join('\n'));
1460
+ }
1461
+
1462
+ function packageManagerFromPackage(pkg = {}) {
1463
+ const value = String(pkg.packageManager || '');
1464
+ if (value.startsWith('pnpm')) return 'pnpm';
1465
+ if (value.startsWith('yarn')) return 'yarn';
1466
+ if (value.startsWith('bun')) return 'bun';
1467
+ return 'npm';
1468
+ }
1469
+
1470
+ function runCommand(pm, script) {
1471
+ if (!script) return null;
1472
+ if (pm === 'npm') return `npm run ${script}`;
1473
+ if (pm === 'yarn') return `yarn ${script}`;
1474
+ if (pm === 'bun') return `bun run ${script}`;
1475
+ return `${pm} run ${script}`;
1476
+ }
1477
+
1478
+ function pickScript(scripts = {}, groups = []) {
1479
+ const names = Object.keys(scripts);
1480
+ for (const group of groups) {
1481
+ const exact = names.find((name) => name === group);
1482
+ if (exact) return exact;
1483
+ }
1484
+ for (const group of groups) {
1485
+ const partial = names.find((name) => name.toLowerCase().includes(group));
1486
+ if (partial) return partial;
1487
+ }
1488
+ return null;
1489
+ }
1490
+
1491
+ async function detectProject(targetDir = process.cwd(), { quiet = false } = {}) {
1492
+ const dir = path.resolve(targetDir || process.cwd());
1493
+ const packageJson = path.join(dir, 'package.json');
1494
+ const result = {
1495
+ dir,
1496
+ packageJson: await exists(packageJson),
1497
+ packageManager: 'npm',
1498
+ commands: {
1499
+ dev: null,
1500
+ typecheck: null,
1501
+ lint: null,
1502
+ test: null,
1503
+ build: null
1504
+ }
1505
+ };
1506
+ if (!result.packageJson) {
1507
+ if (!quiet) {
1508
+ console.log(`No package.json found in ${dir}`);
1509
+ console.log('Suggested verification: use project pack or local framework conventions.');
1510
+ }
1511
+ return result;
1512
+ }
1513
+
1514
+ const pkg = await readJson(packageJson);
1515
+ const scripts = pkg.scripts || {};
1516
+ result.packageManager = packageManagerFromPackage(pkg);
1517
+ result.commands.dev = runCommand(result.packageManager, pickScript(scripts, ['dev', 'start']));
1518
+ result.commands.typecheck = runCommand(result.packageManager, pickScript(scripts, ['typecheck', 'type-check', 'tsc']));
1519
+ result.commands.lint = runCommand(result.packageManager, pickScript(scripts, ['lint']));
1520
+ result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
1521
+ result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
1522
+
1523
+ if (!quiet) printDetection(result);
1524
+ return result;
1525
+ }
1526
+
1527
+ function printDetection(result) {
1528
+ console.log(`Project: ${result.dir}`);
1529
+ console.log(`Package manager: ${result.packageManager}`);
1530
+ console.log('');
1531
+ console.log('Detected commands:');
1532
+ for (const [key, value] of Object.entries(result.commands)) {
1533
+ console.log(`- ${key}: ${value || '(not found)'}`);
1534
+ }
1535
+ console.log('');
1536
+ console.log('Smallest useful checks:');
1537
+ console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
1538
+ console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
1539
+ console.log(`- $deep: ${[result.commands.typecheck, result.commands.lint, result.commands.test, result.commands.build].filter(Boolean).join(' && ') || `No command detected; define in ${PROJECT_PACK}`}`);
1540
+ }
1541
+
1542
+ function budget(routeArg = '') {
1543
+ const normalized = normalizeRoute(routeArg);
1544
+ const entries = normalized ? [[normalized, ROUTE_BUDGETS[normalized]]] : Object.entries(ROUTE_BUDGETS);
1545
+ if (normalized && !ROUTE_BUDGETS[normalized]) {
1546
+ console.error(`unknown route: ${routeArg}`);
1547
+ process.exitCode = 1;
1548
+ return;
1549
+ }
1550
+ for (const [route, info] of entries) {
1551
+ console.log(`$${route}`);
1552
+ console.log(`- files: ${info.files}`);
1553
+ console.log(`- commands: ${info.commands}`);
1554
+ console.log(`- report: ${info.report}`);
1555
+ console.log(`- expand: ${info.expand}`);
1556
+ console.log(`- limits: files<=${info.limits.files}, commands<=${info.limits.commands}, report-lines<=${info.limits.reportLines}, seconds<=${info.limits.seconds}`);
1557
+ console.log('');
1558
+ }
1559
+ }
1560
+
1561
+ function measure(routeArg = '', args = []) {
1562
+ const normalized = normalizeRoute(routeArg);
1563
+ const info = ROUTE_BUDGETS[normalized];
1564
+ if (!normalized || !info) {
1565
+ console.error('usage: yam measure <route> [--files n] [--commands n] [--report-lines n] [--seconds n]');
1566
+ process.exitCode = 1;
1567
+ return;
1568
+ }
1569
+
1570
+ const actual = parseMeasureArgs(args);
1571
+ const checks = [
1572
+ ['files', actual.files, info.limits.files],
1573
+ ['commands', actual.commands, info.limits.commands],
1574
+ ['report-lines', actual.reportLines, info.limits.reportLines],
1575
+ ['seconds', actual.seconds, info.limits.seconds]
1576
+ ];
1577
+ const measured = checks.filter(([, value]) => Number.isFinite(value));
1578
+ const over = checks.filter(([, value, limit]) => Number.isFinite(value) && value > limit);
1579
+ const missing = checks.filter(([, value]) => !Number.isFinite(value));
1580
+
1581
+ console.log(`Token budget report: $${normalized}`);
1582
+ console.log(`Status: ${over.length ? 'over budget' : measured.length ? 'ok' : 'no measurements'}`);
1583
+ console.log('');
1584
+ console.log('Budget:');
1585
+ console.log(`- files: ${info.files}`);
1586
+ console.log(`- commands: ${info.commands}`);
1587
+ console.log(`- report: ${info.report}`);
1588
+ console.log(`- expand: ${info.expand}`);
1589
+ console.log('');
1590
+ console.log('Actual:');
1591
+ for (const [label, value, limit] of checks) {
1592
+ const display = Number.isFinite(value) ? value : '(not measured)';
1593
+ const mark = Number.isFinite(value) && value > limit ? ' over' : '';
1594
+ console.log(`- ${label}: ${display} / limit ${limit}${mark}`);
1595
+ }
1596
+
1597
+ if (missing.length) {
1598
+ console.log('');
1599
+ console.log(`Missing measurements: ${missing.map(([label]) => label).join(', ')}`);
1600
+ }
1601
+ if (over.length) {
1602
+ console.log('');
1603
+ console.log('Reduce next run:');
1604
+ for (const [label] of over) {
1605
+ if (label === 'files') console.log('- Read the project pack first, then only the edit surface.');
1606
+ if (label === 'commands') console.log('- Prefer the smallest honest check before build/deep verification.');
1607
+ if (label === 'report-lines') console.log('- Move detail into remaining tasks or fix-first items.');
1608
+ if (label === 'seconds') console.log('- Use a narrower route or switch heavy work to explicit deep/mission.');
1609
+ }
1610
+ process.exitCode = 1;
1611
+ }
1612
+ }
1613
+
1614
+ function parseMeasureArgs(args = []) {
1615
+ const actual = {
1616
+ files: NaN,
1617
+ commands: NaN,
1618
+ reportLines: NaN,
1619
+ seconds: NaN
1620
+ };
1621
+ const aliases = new Map([
1622
+ ['--files', 'files'],
1623
+ ['--file-count', 'files'],
1624
+ ['--commands', 'commands'],
1625
+ ['--command-count', 'commands'],
1626
+ ['--report-lines', 'reportLines'],
1627
+ ['--lines', 'reportLines'],
1628
+ ['--seconds', 'seconds'],
1629
+ ['--secs', 'seconds']
1630
+ ]);
1631
+
1632
+ for (let index = 0; index < args.length; index += 1) {
1633
+ const arg = args[index];
1634
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1635
+ const key = aliases.get(rawKey);
1636
+ if (!key) continue;
1637
+ const value = inlineValue ?? args[index + 1];
1638
+ if (inlineValue === undefined) index += 1;
1639
+ actual[key] = Number(value);
1640
+ }
1641
+
1642
+ return actual;
1643
+ }
1644
+
1645
+ function normalizeRoute(value = '') {
1646
+ const route = String(value || '').trim().replace(/^\$/, '');
1647
+ if (!route) return '';
1648
+ return route.replace(/^yam-/, '').replace(/^timeto-/, '');
1649
+ }
1650
+
1651
+ async function printTemplate(name = '') {
1652
+ const key = String(name || '').trim().toLowerCase();
1653
+ const map = {
1654
+ project: PROJECT_PACK,
1655
+ ueye: 'ueye-review.md',
1656
+ mission: 'mission-plan.md',
1657
+ proof: 'runtime-proof.md',
1658
+ runtime: 'runtime-proof.md',
1659
+ tuning: 'tuning-log.md'
1660
+ };
1661
+ const file = map[key];
1662
+ if (!file) {
1663
+ console.error('usage: yam template <project|ueye|mission|proof|tuning>');
1664
+ process.exitCode = 1;
1665
+ return;
1666
+ }
1667
+ console.log(await readText(path.join(ROOT, 'templates', file)));
1668
+ }
1669
+
1670
+ async function tuneLog(targetDir = process.cwd()) {
1671
+ const resolved = path.resolve(targetDir || process.cwd());
1672
+ const dir = path.join(resolved, '.yam');
1673
+ const target = path.join(dir, 'tuning-log.md');
1674
+ await fsp.mkdir(dir, { recursive: true });
1675
+ if (await exists(target)) {
1676
+ console.log(`tuning log already exists: ${target}`);
1677
+ return;
1678
+ }
1679
+ await fsp.copyFile(path.join(ROOT, 'templates', 'tuning-log.md'), target);
1680
+ console.log(`created ${target}`);
1681
+ }
1682
+
1683
+ async function memory(args = []) {
1684
+ const subcommand = args[0] || 'list';
1685
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return memoryUsage();
1686
+ if (subcommand === 'init') return memoryInit(args[1]);
1687
+ if (subcommand === 'add') return memoryAdd(args.slice(1));
1688
+ if (subcommand === 'list') {
1689
+ const parsed = parseMemoryArgs(args.slice(1));
1690
+ return memoryList(parsed.dir, { json: args.includes('--json') });
1691
+ }
1692
+ if (subcommand === 'summary' || subcommand === 'summarize') {
1693
+ const parsed = parseMemoryArgs(args.slice(1));
1694
+ return memorySummary(parsed.dir);
1695
+ }
1696
+ if (subcommand === 'resolve') return memoryResolve(args.slice(1));
1697
+ console.error(`unknown memory command: ${subcommand}`);
1698
+ return memoryUsage();
1699
+ }
1700
+
1701
+ function memoryUsage() {
1702
+ console.log(`yam memory
1703
+
1704
+ Usage:
1705
+ yam memory init [dir]
1706
+ yam memory add [dir] --kind <kind> --summary <text> [--evidence <text>] [--action <text>] [--source <text>]
1707
+ yam memory list [dir] [--json]
1708
+ yam memory summary [dir]
1709
+ yam memory resolve [dir] <id> [--note <text>]
1710
+
1711
+ Kinds:
1712
+ wrong_decision, repeat_mistake, direction_change, lesson, risk, command
1713
+
1714
+ Notes:
1715
+ Memory is opt-in and project-local under .yam/memory/.
1716
+ `);
1717
+ }
1718
+
1719
+ async function memoryInit(targetDir = process.cwd()) {
1720
+ const dir = memoryDir(targetDir);
1721
+ await fsp.mkdir(path.join(dir, 'records'), { recursive: true });
1722
+ const readme = path.join(dir, 'README.md');
1723
+ if (!await exists(readme)) {
1724
+ await fsp.writeFile(readme, [
1725
+ '# yam Memory',
1726
+ '',
1727
+ 'Opt-in project memory for short records about wrong decisions, repeated mistakes, direction changes, lessons, risks, and command notes.',
1728
+ '',
1729
+ 'Records live in `records/*.json`. Regenerate `summary.md` with `yam memory summary .`.',
1730
+ ''
1731
+ ].join('\n'));
1732
+ }
1733
+ console.log(`memory ready: ${dir}`);
1734
+ }
1735
+
1736
+ async function memoryAdd(args = []) {
1737
+ const parsed = parseMemoryArgs(args);
1738
+ const dir = memoryDir(parsed.dir);
1739
+ const recordsDir = path.join(dir, 'records');
1740
+ const kind = parsed.flags.kind || 'lesson';
1741
+ const allowedKinds = new Set(['wrong_decision', 'repeat_mistake', 'direction_change', 'lesson', 'risk', 'command']);
1742
+ const summary = parsed.flags.summary || parsed.flags.note || '';
1743
+ const evidence = parsed.flags.evidence || '';
1744
+ const action = parsed.flags.action || parsed.flags.recommendation || '';
1745
+ const source = parsed.flags.source || '';
1746
+
1747
+ if (!allowedKinds.has(kind)) {
1748
+ console.error(`invalid memory kind: ${kind}`);
1749
+ console.error(`allowed: ${[...allowedKinds].join(', ')}`);
1750
+ process.exitCode = 1;
1751
+ return;
1752
+ }
1753
+ if (!summary.trim()) {
1754
+ console.error('missing required --summary');
1755
+ process.exitCode = 1;
1756
+ return;
1757
+ }
1758
+ const secretHit = findSensitivePattern([summary, evidence, action, source].join('\n'));
1759
+ if (secretHit) {
1760
+ console.error(`memory entry blocked: possible secret pattern detected (${secretHit})`);
1761
+ process.exitCode = 1;
1762
+ return;
1763
+ }
1764
+
1765
+ await fsp.mkdir(recordsDir, { recursive: true });
1766
+ const id = `mem-${timestampId()}`;
1767
+ const record = {
1768
+ schemaVersion: 1,
1769
+ id,
1770
+ kind,
1771
+ status: 'active',
1772
+ summary: summary.trim(),
1773
+ evidence: evidence.trim(),
1774
+ action: action.trim(),
1775
+ source: source.trim(),
1776
+ createdAt: new Date().toISOString()
1777
+ };
1778
+ await fsp.writeFile(path.join(recordsDir, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`);
1779
+ console.log(`memory added: ${id}`);
1780
+ }
1781
+
1782
+ async function memoryList(targetDir = process.cwd(), { json = false } = {}) {
1783
+ const records = await readMemoryRecords(targetDir);
1784
+ if (json) {
1785
+ console.log(JSON.stringify(records, null, 2));
1786
+ return;
1787
+ }
1788
+ if (!records.length) {
1789
+ console.log(`No memory records found in ${memoryDir(targetDir)}`);
1790
+ return;
1791
+ }
1792
+ for (const record of records) {
1793
+ console.log(`${record.id} [${record.status}] ${record.kind}: ${record.summary}`);
1794
+ if (record.action) console.log(` action: ${record.action}`);
1795
+ }
1796
+ }
1797
+
1798
+ async function memorySummary(targetDir = process.cwd()) {
1799
+ const dir = memoryDir(targetDir);
1800
+ const records = await readMemoryRecords(targetDir);
1801
+ await fsp.mkdir(dir, { recursive: true });
1802
+ const active = records.filter((record) => record.status !== 'resolved');
1803
+ const resolved = records.filter((record) => record.status === 'resolved');
1804
+ const lines = [
1805
+ '# yam Memory Summary',
1806
+ '',
1807
+ `Generated: ${new Date().toISOString()}`,
1808
+ '',
1809
+ 'This summary is generated from opt-in `.yam/memory/records/*.json` files. Keep it sparse and do not treat it as automatic truth.',
1810
+ '',
1811
+ `Active records: ${active.length}`,
1812
+ `Resolved records: ${resolved.length}`,
1813
+ ''
1814
+ ];
1815
+
1816
+ for (const kind of ['wrong_decision', 'repeat_mistake', 'direction_change', 'lesson', 'risk', 'command']) {
1817
+ const group = active.filter((record) => record.kind === kind);
1818
+ if (!group.length) continue;
1819
+ lines.push(`## ${kind}`);
1820
+ lines.push('');
1821
+ for (const record of group) {
1822
+ lines.push(`- ${record.summary} (${record.id})`);
1823
+ if (record.action) lines.push(` - Next action: ${record.action}`);
1824
+ if (record.evidence) lines.push(` - Evidence: ${record.evidence}`);
1825
+ }
1826
+ lines.push('');
1827
+ }
1828
+
1829
+ const target = path.join(dir, 'summary.md');
1830
+ await fsp.writeFile(target, `${lines.join('\n').trim()}\n`);
1831
+ console.log(`memory summary written: ${target}`);
1832
+ }
1833
+
1834
+ async function memoryResolve(args = []) {
1835
+ const parsed = parseMemoryArgs(args);
1836
+ const id = parsed.positionals[0];
1837
+ if (!id) {
1838
+ console.error('missing memory id');
1839
+ process.exitCode = 1;
1840
+ return;
1841
+ }
1842
+ const recordsDir = path.join(memoryDir(parsed.dir), 'records');
1843
+ const target = path.join(recordsDir, `${id}.json`);
1844
+ if (!await exists(target)) {
1845
+ console.error(`memory record not found: ${id}`);
1846
+ process.exitCode = 1;
1847
+ return;
1848
+ }
1849
+ const record = await readJson(target);
1850
+ record.status = 'resolved';
1851
+ record.resolvedAt = new Date().toISOString();
1852
+ record.resolution = (parsed.flags.note || parsed.flags.reason || '').trim();
1853
+ await fsp.writeFile(target, `${JSON.stringify(record, null, 2)}\n`);
1854
+ console.log(`memory resolved: ${id}`);
1855
+ }
1856
+
1857
+ function parseMemoryArgs(args = []) {
1858
+ const flags = {};
1859
+ const positionals = [];
1860
+ let dir = process.cwd();
1861
+
1862
+ for (let index = 0; index < args.length; index += 1) {
1863
+ const arg = args[index];
1864
+ if (arg.startsWith('--')) {
1865
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1866
+ const key = rawKey.slice(2).replace(/-/g, '_');
1867
+ const value = inlineValue ?? args[index + 1] ?? '';
1868
+ flags[key] = value;
1869
+ if (inlineValue === undefined) index += 1;
1870
+ continue;
1871
+ }
1872
+ if (dir === process.cwd() && looksLikeDirectoryArg(arg)) dir = arg;
1873
+ else positionals.push(arg);
1874
+ }
1875
+
1876
+ return { dir, flags, positionals };
1877
+ }
1878
+
1879
+ function looksLikeDirectoryArg(value = '') {
1880
+ return value === '.' || value.startsWith('/') || value.startsWith('~') || value.startsWith('./') || value.startsWith('../');
1881
+ }
1882
+
1883
+ function memoryDir(targetDir = process.cwd()) {
1884
+ const expanded = String(targetDir || process.cwd()).replace(/^~(?=$|\/)/, os.homedir());
1885
+ return path.join(path.resolve(expanded), '.yam', 'memory');
1886
+ }
1887
+
1888
+ async function readMemoryRecords(targetDir = process.cwd()) {
1889
+ const recordsDir = path.join(memoryDir(targetDir), 'records');
1890
+ if (!await exists(recordsDir)) return [];
1891
+ const entries = await fsp.readdir(recordsDir, { withFileTypes: true });
1892
+ const records = [];
1893
+ for (const entry of entries) {
1894
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
1895
+ const file = path.join(recordsDir, entry.name);
1896
+ try {
1897
+ const record = await readJson(file);
1898
+ records.push(record);
1899
+ } catch (error) {
1900
+ records.push({ id: entry.name.replace(/\.json$/, ''), kind: 'invalid', status: 'invalid', summary: `invalid JSON: ${error.message}` });
1901
+ }
1902
+ }
1903
+ return records.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
1904
+ }
1905
+
1906
+ function timestampId() {
1907
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.(\d{3})Z$/, '$1z');
1908
+ const suffix = Math.random().toString(36).slice(2, 6);
1909
+ return `${stamp}-${suffix}`;
1910
+ }
1911
+
1912
+ function findSensitivePattern(text = '') {
1913
+ const patterns = [
1914
+ ['private_key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/i],
1915
+ ['openai_key', /\bsk-[A-Za-z0-9_-]{20,}\b/],
1916
+ ['github_token', /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/],
1917
+ ['aws_access_key', /\bAKIA[0-9A-Z]{16}\b/],
1918
+ ['password_assignment', /\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/i]
1919
+ ];
1920
+ const hit = patterns.find(([, pattern]) => pattern.test(text));
1921
+ return hit ? hit[0] : '';
1922
+ }
1923
+
1924
+ function showPath() {
1925
+ console.log(`root: ${ROOT}`);
1926
+ console.log(`skills: ${DEST}`);
1927
+ console.log(`codex mirror cleanup: ${CODEX_MIRROR}`);
1928
+ }
1929
+
1930
+ async function main() {
1931
+ const command = process.argv[2] || 'help';
1932
+ if (command === 'help' || command === '--help' || command === '-h') return usage();
1933
+ if (command === 'install') return install();
1934
+ if (command === 'uninstall') return uninstall();
1935
+ if (command === 'version') return console.log(VERSION);
1936
+ if (command === 'detect') return detectProject(process.argv[3]);
1937
+ if (command === 'pack') return inspectProjectPack(process.argv[3]);
1938
+ if (command === 'budget') return budget(process.argv[3]);
1939
+ if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
1940
+ if (command === 'tools') return tools(process.argv.slice(3));
1941
+ if (command === 'proof') return proof(process.argv.slice(3));
1942
+ if (command === 'safety') return safety(process.argv.slice(3));
1943
+ if (command === 'memory') return memory(process.argv.slice(3));
1944
+ if (command === 'hook') return hook(process.argv.slice(3));
1945
+ if (command === 'template') return printTemplate(process.argv[3]);
1946
+ if (command === 'tune-log') return tuneLog(process.argv[3]);
1947
+ if (command === 'status') {
1948
+ const missing = await status();
1949
+ if (missing > 0) process.exitCode = 1;
1950
+ return;
1951
+ }
1952
+ if (command === 'list') return list();
1953
+ if (command === 'verify') return verify();
1954
+ if (command === 'doctor') return doctor();
1955
+ if (command === 'examples') return examples();
1956
+ if (command === 'path') return showPath();
1957
+ if (command === 'init-project') return initProject(process.argv[3]);
1958
+ console.error(`unknown command: ${command}`);
1959
+ usage();
1960
+ process.exitCode = 1;
1961
+ }
1962
+
1963
+ main().catch((error) => {
1964
+ console.error(error?.message || error);
1965
+ process.exitCode = 1;
1966
+ });