youmd 0.8.6 → 0.8.11

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 (44) hide show
  1. package/dist/commands/agent.d.ts +4 -0
  2. package/dist/commands/agent.d.ts.map +1 -1
  3. package/dist/commands/agent.js +4 -0
  4. package/dist/commands/agent.js.map +1 -1
  5. package/dist/commands/machine.d.ts.map +1 -1
  6. package/dist/commands/machine.js +145 -2
  7. package/dist/commands/machine.js.map +1 -1
  8. package/dist/commands/project.d.ts.map +1 -1
  9. package/dist/commands/project.js +31 -0
  10. package/dist/commands/project.js.map +1 -1
  11. package/dist/commands/skill.d.ts.map +1 -1
  12. package/dist/commands/skill.js +679 -1
  13. package/dist/commands/skill.js.map +1 -1
  14. package/dist/commands/stack.d.ts.map +1 -1
  15. package/dist/commands/stack.js +97 -1
  16. package/dist/commands/stack.js.map +1 -1
  17. package/dist/commands/sync.d.ts.map +1 -1
  18. package/dist/commands/sync.js +84 -3
  19. package/dist/commands/sync.js.map +1 -1
  20. package/dist/index.js +10 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/api.d.ts +179 -0
  23. package/dist/lib/api.d.ts.map +1 -1
  24. package/dist/lib/api.js +41 -4
  25. package/dist/lib/api.js.map +1 -1
  26. package/dist/lib/machine-bootstrap-prompt.d.ts.map +1 -1
  27. package/dist/lib/machine-bootstrap-prompt.js +58 -22
  28. package/dist/lib/machine-bootstrap-prompt.js.map +1 -1
  29. package/dist/lib/machine-verify.d.ts +27 -0
  30. package/dist/lib/machine-verify.d.ts.map +1 -1
  31. package/dist/lib/machine-verify.js +84 -0
  32. package/dist/lib/machine-verify.js.map +1 -1
  33. package/dist/lib/realtime-sync.d.ts +26 -0
  34. package/dist/lib/realtime-sync.d.ts.map +1 -1
  35. package/dist/lib/realtime-sync.js +44 -1
  36. package/dist/lib/realtime-sync.js.map +1 -1
  37. package/dist/mcp/registry.d.ts +2 -1
  38. package/dist/mcp/registry.d.ts.map +1 -1
  39. package/dist/mcp/registry.js +63 -1
  40. package/dist/mcp/registry.js.map +1 -1
  41. package/package.json +1 -1
  42. package/scripts/local-agent-stack-inventory.mjs +1150 -0
  43. package/scripts/skillstack-sync/README.md +11 -3
  44. package/skills/machine-bootstrap.md +4 -3
@@ -0,0 +1,1150 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+
7
+ const home = os.homedir();
8
+ const repoRoot = process.cwd();
9
+ const now = new Date();
10
+
11
+ function parseArgs(argv) {
12
+ const opts = {};
13
+ for (let i = 0; i < argv.length; i += 1) {
14
+ const arg = argv[i];
15
+ if (arg === "--out-dir") opts.outDir = argv[++i];
16
+ else if (arg === "--workspace") opts.workspace = argv[++i];
17
+ else if (arg === "--date") opts.date = argv[++i];
18
+ else if (arg === "--progress") opts.progress = true;
19
+ else if (arg === "--help" || arg === "-h") opts.help = true;
20
+ }
21
+ return opts;
22
+ }
23
+
24
+ const cli = parseArgs(process.argv.slice(2));
25
+ if (cli.help) {
26
+ console.log(`Usage: node scripts/local-agent-stack-inventory.mjs [--out-dir DIR] [--workspace DIR] [--date YYYY-MM-DD] [--progress]
27
+
28
+ Secret-safe inventory of Houston's local/global agent skills, prompts,
29
+ preferences, project context, host mirrors, and sync/catalog gaps.
30
+ `);
31
+ process.exit(0);
32
+ }
33
+
34
+ const stamp = cli.date || now.toISOString().slice(0, 10);
35
+ const workspaceRoot = path.resolve(cli.workspace || path.join(home, "Desktop", "CODE_2025"));
36
+ const outDir = path.resolve(cli.outDir || path.join(repoRoot, "project-context"));
37
+ fs.mkdirSync(outDir, { recursive: true });
38
+
39
+ const outJson = path.join(outDir, `local-agent-stack-inventory-${stamp}.json`);
40
+ const outHtml = path.join(outDir, `local-agent-stack-inventory-${stamp}.html`);
41
+
42
+ const roots = {
43
+ agentShared: path.join(home, ".agent-shared"),
44
+ sharedSkills: path.join(home, ".agent-shared", "claude-skills"),
45
+ sharedAgentConfig: path.join(home, ".agent-shared", "agent-config"),
46
+ claudeSkills: path.join(home, ".claude", "skills"),
47
+ claudeProjects: path.join(home, ".claude", "projects"),
48
+ codexSkills: path.join(home, ".codex", "skills"),
49
+ codexUpperSkills: path.join(home, ".Codex", "skills"),
50
+ codexPluginSkills: path.join(home, ".codex", "plugins", "cache"),
51
+ agentsSkills: path.join(home, ".agents", "skills"),
52
+ cursorRoot: path.join(home, ".cursor"),
53
+ piAgent: path.join(home, ".pi", "agent"),
54
+ scienceStack: path.join(home, ".claude", "scistack"),
55
+ gstack: path.join(home, ".claude", "skills", "gstack"),
56
+ youmdHome: path.join(home, ".youmd"),
57
+ youmdSkills: path.join(home, ".youmd", "skills"),
58
+ workspace: workspaceRoot,
59
+ };
60
+
61
+ const skipDirs = new Set([
62
+ ".git",
63
+ "node_modules",
64
+ ".next",
65
+ ".cache",
66
+ ".gradle",
67
+ ".idea",
68
+ ".mypy_cache",
69
+ ".parcel-cache",
70
+ ".pnpm-store",
71
+ ".pytest_cache",
72
+ ".ruff_cache",
73
+ ".serverless",
74
+ ".venv",
75
+ ".vscode",
76
+ ".yarn",
77
+ "__pycache__",
78
+ "coverage",
79
+ "dist",
80
+ "build",
81
+ "env",
82
+ "logs",
83
+ "Pods",
84
+ ".turbo",
85
+ "target",
86
+ "tmp",
87
+ "venv",
88
+ "DerivedData",
89
+ "Library",
90
+ "Cache",
91
+ "Caches",
92
+ "CachedData",
93
+ ]);
94
+
95
+ const projectSignalSkipDirs = new Set([
96
+ ...skipDirs,
97
+ ".agents",
98
+ ".claude",
99
+ ".codex",
100
+ ".Codex",
101
+ ".cursor",
102
+ ".pi",
103
+ ]);
104
+
105
+ const walkIssues = [];
106
+ const defaultWalkMaxEntries = Number.parseInt(process.env.YOUMD_AGENT_STACK_INVENTORY_WALK_MAX_ENTRIES || "200000", 10);
107
+ const defaultWalkMaxMs = Number.parseInt(process.env.YOUMD_AGENT_STACK_INVENTORY_WALK_MAX_MS || "10000", 10);
108
+ const directEntriesCache = new Map();
109
+ const skillFilesCache = new Map();
110
+ const progressEnabled = Boolean(cli.progress || process.env.YOUMD_AGENT_STACK_INVENTORY_PROGRESS === "1");
111
+ const progressPrefix = "youmd-inventory-progress ";
112
+ const phaseTimings = [];
113
+
114
+ function emitProgress(event) {
115
+ if (!progressEnabled) return;
116
+ try {
117
+ process.stderr.write(progressPrefix + JSON.stringify(event) + "\n");
118
+ } catch {
119
+ // Progress is best-effort; inventory output must still complete.
120
+ }
121
+ }
122
+
123
+ function withPhase(name, label, fn) {
124
+ const start = Date.now();
125
+ emitProgress({ event: "start", phase: name, label });
126
+ try {
127
+ const result = fn();
128
+ const durationMs = Date.now() - start;
129
+ phaseTimings.push({ phase: name, label, durationMs });
130
+ emitProgress({ event: "done", phase: name, label, durationMs });
131
+ return result;
132
+ } catch (err) {
133
+ const durationMs = Date.now() - start;
134
+ phaseTimings.push({ phase: name, label, durationMs, failed: true });
135
+ emitProgress({
136
+ event: "failed",
137
+ phase: name,
138
+ label,
139
+ durationMs,
140
+ message: err instanceof Error ? err.message : String(err),
141
+ });
142
+ throw err;
143
+ }
144
+ }
145
+
146
+ function exists(p) {
147
+ try {
148
+ return fs.existsSync(p);
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ function statSafe(p) {
155
+ try {
156
+ return fs.lstatSync(p);
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function realpathSafe(p) {
163
+ try {
164
+ return fs.realpathSync(p);
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ function readJsonSafe(p) {
171
+ try {
172
+ return JSON.parse(fs.readFileSync(p, "utf8"));
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ function readTextPrefix(p, max = 12000) {
179
+ try {
180
+ const fd = fs.openSync(p, "r");
181
+ const buffer = Buffer.alloc(max);
182
+ const bytes = fs.readSync(fd, buffer, 0, max, 0);
183
+ fs.closeSync(fd);
184
+ return buffer.subarray(0, bytes).toString("utf8");
185
+ } catch {
186
+ return "";
187
+ }
188
+ }
189
+
190
+ function relHome(p) {
191
+ if (!p) return null;
192
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
193
+ }
194
+
195
+ function walk(root, predicate, options = {}) {
196
+ const maxDepth = options.maxDepth ?? Infinity;
197
+ const followSymlinks = options.followSymlinks ?? false;
198
+ const maxEntries = options.maxEntries ?? defaultWalkMaxEntries;
199
+ const maxMs = options.maxMs ?? defaultWalkMaxMs;
200
+ const deadline = Date.now() + maxMs;
201
+ const seenDirs = new Set();
202
+ const results = [];
203
+ if (!exists(root)) return results;
204
+ let visited = 0;
205
+ let stoppedReason = null;
206
+
207
+ function stop(reason) {
208
+ if (stoppedReason) return;
209
+ stoppedReason = reason;
210
+ walkIssues.push({
211
+ root: relHome(root),
212
+ reason,
213
+ visited,
214
+ maxDepth: Number.isFinite(maxDepth) ? maxDepth : null,
215
+ maxEntries,
216
+ maxMs,
217
+ partialResults: results.length,
218
+ });
219
+ }
220
+
221
+ function visit(p, depth) {
222
+ if (stoppedReason) return;
223
+ visited += 1;
224
+ if (visited > maxEntries) {
225
+ stop("entry-limit");
226
+ return;
227
+ }
228
+ if (Date.now() > deadline) {
229
+ stop("time-limit");
230
+ return;
231
+ }
232
+ const st = statSafe(p);
233
+ if (!st) return;
234
+ if (predicate(p, st)) results.push(p);
235
+ if (depth >= maxDepth) return;
236
+
237
+ let dirSt = st;
238
+ if (st.isSymbolicLink()) {
239
+ if (!followSymlinks) return;
240
+ try {
241
+ dirSt = fs.statSync(p);
242
+ } catch {
243
+ return;
244
+ }
245
+ }
246
+ if (!dirSt.isDirectory()) return;
247
+ if (followSymlinks) {
248
+ const realDir = realpathSafe(p);
249
+ if (realDir) {
250
+ if (seenDirs.has(realDir)) return;
251
+ seenDirs.add(realDir);
252
+ }
253
+ }
254
+
255
+ let entries = [];
256
+ try {
257
+ entries = fs.readdirSync(p, { withFileTypes: true });
258
+ } catch {
259
+ return;
260
+ }
261
+ for (const entry of entries) {
262
+ if (entry.isDirectory() && skipDirs.has(entry.name)) continue;
263
+ visit(path.join(p, entry.name), depth + 1);
264
+ }
265
+ }
266
+
267
+ visit(root, 0);
268
+ return results;
269
+ }
270
+
271
+ function countFiles(root, matcher, options = {}) {
272
+ return walk(root, (p, st) => st.isFile() && matcher(p), options).length;
273
+ }
274
+
275
+ function directEntries(root) {
276
+ if (directEntriesCache.has(root)) return directEntriesCache.get(root);
277
+ if (!exists(root)) return [];
278
+ try {
279
+ const entries = fs.readdirSync(root).map((name) => {
280
+ const full = path.join(root, name);
281
+ const st = statSafe(full);
282
+ const isSymlink = Boolean(st?.isSymbolicLink());
283
+ const target = isSymlink ? fs.readlinkSync(full) : null;
284
+ const real = realpathSafe(full);
285
+ const hasSkill = exists(path.join(full, "SKILL.md"));
286
+ return {
287
+ name,
288
+ path: full,
289
+ pathDisplay: relHome(full),
290
+ kind: isSymlink ? "symlink" : st?.isDirectory() ? "directory" : st?.isFile() ? "file" : "other",
291
+ target: target ? relHome(path.resolve(path.dirname(full), target)) : null,
292
+ realpath: relHome(real),
293
+ hasSkill,
294
+ };
295
+ });
296
+ directEntriesCache.set(root, entries);
297
+ return entries;
298
+ } catch {
299
+ return [];
300
+ }
301
+ }
302
+
303
+ function skillFiles(root) {
304
+ if (skillFilesCache.has(root)) return skillFilesCache.get(root);
305
+ const skills = walk(root, (p, st) => st.isFile() && path.basename(p) === "SKILL.md").map((file) => {
306
+ const dir = path.dirname(file);
307
+ const name = path.basename(dir);
308
+ const real = realpathSafe(file) || file;
309
+ const classification = classifySkill(file, real);
310
+ return {
311
+ name,
312
+ file,
313
+ pathDisplay: relHome(file),
314
+ realpath: relHome(real),
315
+ sourceClass: classification.sourceClass,
316
+ ownerClass: classification.ownerClass,
317
+ provenance: classification.provenance,
318
+ syncPolicy: classification.syncPolicy,
319
+ };
320
+ });
321
+ skillFilesCache.set(root, skills);
322
+ return skills;
323
+ }
324
+
325
+ function directSkillRecords(root) {
326
+ return directEntries(root)
327
+ .filter((entry) => entry.hasSkill)
328
+ .map((entry) => {
329
+ const file = path.join(entry.path, "SKILL.md");
330
+ const real = realpathSafe(file) || file;
331
+ const classification = classifySkill(file, real);
332
+ return {
333
+ name: entry.name,
334
+ file,
335
+ pathDisplay: relHome(file),
336
+ realpath: relHome(real),
337
+ sourceClass: classification.sourceClass,
338
+ ownerClass: classification.ownerClass,
339
+ provenance: classification.provenance,
340
+ syncPolicy: classification.syncPolicy,
341
+ exposureKind: entry.kind,
342
+ };
343
+ });
344
+ }
345
+
346
+ function classifyPath(p) {
347
+ if (!p) return "unknown";
348
+ if (p.startsWith(roots.agentShared)) return "agent-shared";
349
+ if (p.startsWith(roots.scienceStack)) return "scistack";
350
+ if (p.startsWith(roots.gstack)) return "gstack";
351
+ if (p.startsWith(roots.youmdSkills)) return "youmd-catalog";
352
+ if (p.startsWith(path.join(home, ".codex", "plugins", "cache"))) return "codex-plugin-cache";
353
+ if (p.startsWith(path.join(home, ".codex", "skills"))) return "codex-host";
354
+ if (p.startsWith(path.join(home, ".claude", "skills"))) return "claude-host";
355
+ if (p.startsWith(path.join(home, ".agents", "skills"))) return "agents-host";
356
+ return "other";
357
+ }
358
+
359
+ function classifySkill(file, real) {
360
+ const sourceClass = classifyPath(real);
361
+ const text = readTextPrefix(file, 10000).toLowerCase();
362
+ let ownerClass = "unknown";
363
+ let provenance = "unknown";
364
+ let syncPolicy = "review-before-sync";
365
+
366
+ if (real.startsWith(roots.sharedSkills)) {
367
+ ownerClass = "houston-owned-shared";
368
+ provenance = "canonical ~/.agent-shared";
369
+ syncPolicy = "syncable-canonical";
370
+ } else if (real.startsWith(roots.scienceStack)) {
371
+ if (real.includes("/extensions/")) {
372
+ ownerClass = "external-science-extension";
373
+ provenance = "SciStack opt-in upstream extension";
374
+ syncPolicy = "catalog-as-external-reference";
375
+ } else {
376
+ ownerClass = "houston-owned-science";
377
+ provenance = "SciStack/HubStack/AstroStack canonical";
378
+ syncPolicy = "syncable-canonical-with-namespace";
379
+ }
380
+ } else if (real.startsWith(roots.gstack)) {
381
+ ownerClass = "gstack-managed-reference";
382
+ provenance = "GStack local reference stack";
383
+ syncPolicy = "catalog-as-external-reference";
384
+ } else if (real.startsWith(roots.youmdSkills)) {
385
+ ownerClass = "youmd-catalog-cache";
386
+ provenance = "You.md local skill catalog";
387
+ syncPolicy = "already-cataloged-or-cache";
388
+ } else if (real.startsWith(roots.codexPluginSkills)) {
389
+ ownerClass = "plugin-bundled";
390
+ provenance = "Codex/OpenAI plugin cache";
391
+ syncPolicy = "catalog-as-plugin-reference";
392
+ } else if (real.startsWith(roots.agentsSkills) && text.includes("skills.sh")) {
393
+ ownerClass = "public-marketplace-helper";
394
+ provenance = "skills.sh referenced helper";
395
+ syncPolicy = "catalog-as-public-source-reference";
396
+ } else if (real.startsWith(roots.agentsSkills)) {
397
+ ownerClass = "agent-host-local";
398
+ provenance = ".agents host-local skill";
399
+ syncPolicy = "review-before-sync";
400
+ } else if (sourceClass.endsWith("-host")) {
401
+ ownerClass = "host-local-or-mirror";
402
+ provenance = "agent host exposure root";
403
+ syncPolicy = "resolve-canonical-owner-first";
404
+ }
405
+
406
+ if (text.includes("clawhub.ai")) provenance = "clawhub.ai referenced";
407
+ if (text.includes("skills.sh")) provenance = "skills.sh referenced";
408
+ if (text.includes("github.com")) provenance = provenance === "unknown" ? "GitHub referenced" : provenance;
409
+
410
+ return { sourceClass, ownerClass, provenance, syncPolicy };
411
+ }
412
+
413
+ function uniqueByName(items) {
414
+ const map = new Map();
415
+ for (const item of items) {
416
+ const current = map.get(item.name);
417
+ if (!current) {
418
+ map.set(item.name, {
419
+ name: item.name,
420
+ paths: [item.pathDisplay],
421
+ realpaths: [item.realpath],
422
+ classes: [item.sourceClass],
423
+ ownerClasses: [item.ownerClass],
424
+ provenances: [item.provenance],
425
+ syncPolicies: [item.syncPolicy],
426
+ });
427
+ } else {
428
+ current.paths.push(item.pathDisplay);
429
+ if (!current.realpaths.includes(item.realpath)) current.realpaths.push(item.realpath);
430
+ if (!current.classes.includes(item.sourceClass)) current.classes.push(item.sourceClass);
431
+ if (!current.ownerClasses.includes(item.ownerClass)) current.ownerClasses.push(item.ownerClass);
432
+ if (!current.provenances.includes(item.provenance)) current.provenances.push(item.provenance);
433
+ if (!current.syncPolicies.includes(item.syncPolicy)) current.syncPolicies.push(item.syncPolicy);
434
+ }
435
+ }
436
+ return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
437
+ }
438
+
439
+ function rollup(items, field) {
440
+ return items.reduce((acc, item) => {
441
+ const key = item[field] || "unknown";
442
+ acc[key] = (acc[key] || 0) + 1;
443
+ return acc;
444
+ }, {});
445
+ }
446
+
447
+ function buildDryAudit(records, catalogNameSet) {
448
+ const groups = new Map();
449
+ for (const record of records) {
450
+ const group = groups.get(record.name) || [];
451
+ group.push(record);
452
+ groups.set(record.name, group);
453
+ }
454
+
455
+ const duplicateNames = [];
456
+ const mirrors = [];
457
+ const ownedPriority = [];
458
+ for (const [name, group] of groups.entries()) {
459
+ const realpaths = [...new Set(group.map((item) => item.realpath))];
460
+ const owners = [...new Set(group.map((item) => item.ownerClass))];
461
+ const policies = [...new Set(group.map((item) => item.syncPolicy))];
462
+ const row = {
463
+ name,
464
+ occurrences: group.length,
465
+ realpathCount: realpaths.length,
466
+ owners,
467
+ policies,
468
+ cataloged: catalogNameSet.has(name),
469
+ samplePaths: group.map((item) => item.pathDisplay).slice(0, 8),
470
+ };
471
+
472
+ if (owners.some((owner) => owner.startsWith("houston-owned"))) ownedPriority.push(row);
473
+ if (realpaths.length === 1 && group.length > 1) mirrors.push(row);
474
+ if (realpaths.length > 1) duplicateNames.push({
475
+ ...row,
476
+ risk: owners.some((owner) => owner.startsWith("houston-owned"))
477
+ ? "review-only-owned-priority"
478
+ : "possible-redundancy-review",
479
+ });
480
+ }
481
+
482
+ return {
483
+ duplicateNameDifferentRealpaths: duplicateNames.sort((a, b) => b.realpathCount - a.realpathCount || a.name.localeCompare(b.name)),
484
+ sameRealpathMirrors: mirrors.sort((a, b) => b.occurrences - a.occurrences || a.name.localeCompare(b.name)),
485
+ ownedPrioritySkills: ownedPriority.sort((a, b) => a.name.localeCompare(b.name)),
486
+ guidance: [
487
+ "Never auto-delete Houston-owned shared, science, or heavily modified skills.",
488
+ "Same-name/different-realpath rows are review queues, not deletion instructions.",
489
+ "Same-realpath mirrors are usually healthy host exposure, not duplication.",
490
+ "Public or plugin skills should be cataloged as external references unless intentionally forked.",
491
+ ],
492
+ };
493
+ }
494
+
495
+ function getCommand(cmd, args) {
496
+ try {
497
+ return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+
503
+ function readSkillCatalog() {
504
+ const catalogPath = path.join(roots.youmdSkills, "youmd-skills.yaml");
505
+ if (!exists(catalogPath)) return { path: catalogPath, skills: [] };
506
+ try {
507
+ const raw = fs.readFileSync(catalogPath, "utf8");
508
+ const skills = [];
509
+ let current = null;
510
+ let activeArray = null;
511
+ for (const line of raw.split(/\r?\n/)) {
512
+ const nameMatch = line.match(/^\s*-\s+name:\s*(.+?)\s*$/);
513
+ if (nameMatch) {
514
+ if (current) skills.push(current);
515
+ current = { name: unquoteYaml(nameMatch[1]), identity_fields: [], requires: [] };
516
+ activeArray = null;
517
+ continue;
518
+ }
519
+ if (!current) continue;
520
+ const scalar = line.match(/^\s{4}([a-zA-Z_]+):\s*(.*?)\s*$/);
521
+ if (scalar) {
522
+ const [, key, value] = scalar;
523
+ if (value === "[]") {
524
+ current[key] = [];
525
+ activeArray = null;
526
+ } else if (value === "") {
527
+ current[key] = [];
528
+ activeArray = key;
529
+ } else {
530
+ current[key] = key === "installed" ? value === "true" : unquoteYaml(value);
531
+ activeArray = null;
532
+ }
533
+ continue;
534
+ }
535
+ const arrayItem = line.match(/^\s{6}-\s+(.+?)\s*$/);
536
+ if (arrayItem && activeArray) {
537
+ current[activeArray].push(unquoteYaml(arrayItem[1]));
538
+ }
539
+ }
540
+ if (current) skills.push(current);
541
+ return { path: catalogPath, skills };
542
+ } catch {
543
+ return { path: catalogPath, skills: [] };
544
+ }
545
+ }
546
+
547
+ function unquoteYaml(value) {
548
+ const trimmed = String(value || "").trim();
549
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
550
+ return trimmed.slice(1, -1);
551
+ }
552
+ return trimmed;
553
+ }
554
+
555
+ function collectProjectSignals() {
556
+ const signals = [];
557
+ const maxDepth = 5;
558
+ const maxEntries = defaultWalkMaxEntries;
559
+ const maxMs = 30000;
560
+ const deadline = Date.now() + maxMs;
561
+ const queue = [{ dir: roots.workspace, depth: 0 }];
562
+ let visited = 0;
563
+ let stoppedReason = null;
564
+
565
+ while (queue.length > 0 && !stoppedReason) {
566
+ const { dir, depth } = queue.shift();
567
+ visited += 1;
568
+ if (visited > maxEntries) {
569
+ stoppedReason = "entry-limit";
570
+ break;
571
+ }
572
+ if (Date.now() > deadline) {
573
+ stoppedReason = "time-limit";
574
+ break;
575
+ }
576
+
577
+ let entries = [];
578
+ try {
579
+ entries = fs.readdirSync(dir, { withFileTypes: true });
580
+ } catch {
581
+ continue;
582
+ }
583
+
584
+ for (const entry of entries) {
585
+ const full = path.join(dir, entry.name);
586
+ if (entry.isDirectory()) {
587
+ if (entry.name === "project-context") signals.push(full);
588
+ if (depth < maxDepth && !projectSignalSkipDirs.has(entry.name)) {
589
+ queue.push({ dir: full, depth: depth + 1 });
590
+ }
591
+ continue;
592
+ }
593
+ if (entry.isFile() && ["AGENTS.md", "CLAUDE.md", "youstack.json", ".youmd-project"].includes(entry.name)) {
594
+ signals.push(full);
595
+ }
596
+ }
597
+ }
598
+
599
+ if (stoppedReason) {
600
+ walkIssues.push({
601
+ root: relHome(roots.workspace),
602
+ reason: `project-signals-${stoppedReason}`,
603
+ visited,
604
+ maxDepth,
605
+ maxEntries,
606
+ maxMs,
607
+ partialResults: signals.length,
608
+ });
609
+ }
610
+
611
+ const buckets = { agents: 0, claude: 0, projectContext: 0, youstack: 0, youmdProject: 0 };
612
+ for (const p of signals) {
613
+ const base = path.basename(p);
614
+ if (base === "AGENTS.md") buckets.agents += 1;
615
+ if (base === "CLAUDE.md") buckets.claude += 1;
616
+ if (base === "project-context") buckets.projectContext += 1;
617
+ if (base === "youstack.json") buckets.youstack += 1;
618
+ if (base === ".youmd-project") buckets.youmdProject += 1;
619
+ }
620
+ return { count: signals.length, buckets, sample: signals.slice(0, 160).map(relHome) };
621
+ }
622
+
623
+ function linkStatus() {
624
+ const expected = [
625
+ [path.join(home, ".claude", "CLAUDE.md"), path.join(home, ".agent-shared", "AGENTS.md")],
626
+ [path.join(home, ".codex", "AGENTS.md"), path.join(home, ".agent-shared", "AGENTS.md")],
627
+ [path.join(home, ".cursorrules"), path.join(home, ".agent-shared", "AGENTS.md")],
628
+ [path.join(home, ".pi", "agent", "AGENTS.md"), path.join(home, ".agent-shared", "AGENTS.md")],
629
+ ];
630
+ return expected.map(([link, target]) => {
631
+ const st = statSafe(link);
632
+ const actual = st?.isSymbolicLink() ? path.resolve(path.dirname(link), fs.readlinkSync(link)) : null;
633
+ return {
634
+ link: relHome(link),
635
+ target: relHome(target),
636
+ ok: actual === target,
637
+ actual: relHome(actual),
638
+ kind: st?.isSymbolicLink() ? "symlink" : exists(link) ? "file" : "missing",
639
+ };
640
+ });
641
+ }
642
+
643
+ function gitInfo(root) {
644
+ if (!exists(path.join(root, ".git"))) return null;
645
+ return {
646
+ branch: getCommand("git", ["-C", root, "branch", "--show-current"]),
647
+ head: getCommand("git", ["-C", root, "rev-parse", "--short", "HEAD"]),
648
+ dirty: Boolean(getCommand("git", ["-C", root, "status", "--short"])),
649
+ };
650
+ }
651
+
652
+ const catalog = withPhase("catalog", "reading You.md skill catalog", readSkillCatalog);
653
+ const directExposureSkillRecords = withPhase("host-exposure", "scanning agent host exposure roots", () => [
654
+ ...directSkillRecords(roots.claudeSkills),
655
+ ...directSkillRecords(roots.codexSkills),
656
+ ...directSkillRecords(roots.codexUpperSkills),
657
+ ...directSkillRecords(roots.agentsSkills),
658
+ ...directSkillRecords(roots.youmdSkills),
659
+ ]);
660
+ const canonicalSkillFiles = withPhase("canonical-skills", "scanning canonical skill stacks", () => [
661
+ ...skillFiles(roots.sharedSkills),
662
+ ...skillFiles(roots.scienceStack),
663
+ ...skillFiles(roots.gstack),
664
+ ...skillFiles(roots.codexPluginSkills),
665
+ ...skillFiles(roots.youmdSkills),
666
+ ]);
667
+ const allSkillFiles = [
668
+ ...directExposureSkillRecords,
669
+ ...canonicalSkillFiles,
670
+ ];
671
+ const uniqueSkills = withPhase("skill-index", "building ownership and skill index", () => uniqueByName(allSkillFiles));
672
+ const uniqueRealSkillFiles = new Set(allSkillFiles.map((item) => item.realpath || item.pathDisplay)).size;
673
+ const catalogNames = new Set(catalog.skills.map((s) => s.name));
674
+ const filesystemNames = new Set(uniqueSkills.map((s) => s.name));
675
+ const missingFromCatalog = uniqueSkills.filter((s) => !catalogNames.has(s.name));
676
+ const catalogNotFoundInFs = catalog.skills.filter((s) => !filesystemNames.has(s.name));
677
+ const dryAudit = withPhase("dry-audit", "checking mirrors and duplicate skill names", () => buildDryAudit(allSkillFiles, catalogNames));
678
+
679
+ const hostRoots = [
680
+ ["Claude host", roots.claudeSkills],
681
+ ["Codex host", roots.codexSkills],
682
+ ["Codex app host", roots.codexUpperSkills],
683
+ [".agents host", roots.agentsSkills],
684
+ ["You.md catalog cache", roots.youmdSkills],
685
+ ["Shared canonical", roots.sharedSkills],
686
+ ["SciStack canonical", roots.scienceStack],
687
+ ["GStack root", roots.gstack],
688
+ ["Codex plugin cache", roots.codexPluginSkills],
689
+ ];
690
+
691
+ const hostSummaries = withPhase("host-summaries", "summarizing agent exposure roots", () => hostRoots.map(([label, root]) => {
692
+ const direct = directEntries(root);
693
+ const skills = skillFiles(root);
694
+ return {
695
+ label,
696
+ root: relHome(root),
697
+ exists: exists(root),
698
+ directEntries: direct.length,
699
+ directSkillEntries: direct.filter((entry) => entry.hasSkill).length,
700
+ skillFiles: skills.length,
701
+ symlinks: direct.filter((entry) => entry.kind === "symlink").length,
702
+ brokenSymlinks: direct.filter((entry) => entry.kind === "symlink" && !exists(entry.path)).length,
703
+ sourceClasses: skills.reduce((acc, item) => {
704
+ acc[item.sourceClass] = (acc[item.sourceClass] || 0) + 1;
705
+ return acc;
706
+ }, {}),
707
+ ownerClasses: rollup(skills, "ownerClass"),
708
+ syncPolicies: rollup(skills, "syncPolicy"),
709
+ sampleEntries: direct.slice(0, 80).map((entry) => ({
710
+ name: entry.name,
711
+ kind: entry.kind,
712
+ target: entry.target,
713
+ hasSkill: entry.hasSkill,
714
+ })),
715
+ };
716
+ }));
717
+
718
+ const promptContext = withPhase("prompt-context", "counting prompts, memories, logs, and local context", () => ({
719
+ youmdIdentityFiles: {
720
+ profile: countFiles(path.join(roots.youmdHome, "profile"), (p) => p.endsWith(".md")),
721
+ preferences: countFiles(path.join(roots.youmdHome, "preferences"), (p) => p.endsWith(".md")),
722
+ voice: countFiles(path.join(roots.youmdHome, "voice"), (p) => p.endsWith(".md")),
723
+ directives: countFiles(path.join(roots.youmdHome, "directives"), (p) => p.endsWith(".md")),
724
+ private: countFiles(path.join(roots.youmdHome, "private"), (p) => p.endsWith(".md")),
725
+ },
726
+ youmdLogs: countFiles(path.join(roots.youmdHome, "logs"), () => true, { maxDepth: 1 }),
727
+ machineReports: countFiles(path.join(roots.youmdHome, "machine-reports"), (p) => p.endsWith(".json"), { maxDepth: 1 }),
728
+ claudeProjectMemories: walk(roots.claudeProjects, (p, st) => st.isFile() && /(^memory$|MEMORY\.md$)/.test(path.basename(p)), { maxDepth: 4 }).length,
729
+ codexJsonlLower: countFiles(path.join(home, ".codex", "projects"), (p) => p.endsWith(".jsonl")),
730
+ codexJsonlUpper: countFiles(path.join(home, ".Codex", "projects"), (p) => p.endsWith(".jsonl")),
731
+ codexAutomationMemories: countFiles(path.join(home, ".codex", "automations"), (p) => path.basename(p) === "memory.md"),
732
+ cursorPlans: countFiles(path.join(home, ".cursor", "plans"), (p) => p.endsWith(".md"), { maxDepth: 1 }),
733
+ cursorBrowserLogs: countFiles(path.join(home, ".cursor", "browser-logs"), (p) => p.endsWith(".log"), { maxDepth: 1 }),
734
+ cursorAgentTranscripts: countFiles(path.join(home, ".cursor", "projects"), (p) => p.includes("agent-transcripts")),
735
+ }));
736
+
737
+ const projectSignals = withPhase("project-signals", "scanning workspace project context markers", collectProjectSignals);
738
+ const localPackage = readJsonSafe(path.join(repoRoot, "cli", "package.json")) || readJsonSafe(path.join(repoRoot, "package.json"));
739
+ const rootPackage = readJsonSafe(path.join(repoRoot, "package.json"));
740
+ const machineReport = readJsonSafe(path.join(roots.youmdHome, "machine-reports", "latest.json"));
741
+ const installedYoumdVersion = withPhase("machine-proof", "reading local machine proof metadata", () => getCommand("youmd", ["--version"]));
742
+
743
+ const summary = {
744
+ generatedAt: now.toISOString(),
745
+ host: os.hostname(),
746
+ repoRoot,
747
+ roots: Object.fromEntries(Object.entries(roots).map(([key, value]) => [key, relHome(value)])),
748
+ versions: {
749
+ installedYoumd: installedYoumdVersion,
750
+ repoPackage: rootPackage?.version || null,
751
+ cliPackage: localPackage?.version || null,
752
+ node: process.version,
753
+ },
754
+ git: {
755
+ youmd: gitInfo(repoRoot),
756
+ agentShared: gitInfo(roots.agentShared),
757
+ },
758
+ totals: {
759
+ uniqueSkillNames: uniqueSkills.length,
760
+ skillFileOccurrences: allSkillFiles.length,
761
+ uniqueRealSkillFiles,
762
+ directExposureSkillRecords: directExposureSkillRecords.length,
763
+ canonicalSkillFiles: canonicalSkillFiles.length,
764
+ duplicateNameDifferentRealpaths: dryAudit.duplicateNameDifferentRealpaths.length,
765
+ sameRealpathMirrors: dryAudit.sameRealpathMirrors.length,
766
+ youmdCatalogSkills: catalog.skills.length,
767
+ youmdCatalogInstalled: catalog.skills.filter((s) => s.installed).length,
768
+ missingFromYoumdCatalog: missingFromCatalog.length,
769
+ catalogNotFoundInFilesystem: catalogNotFoundInFs.length,
770
+ projectSignals: projectSignals.count,
771
+ },
772
+ hostSummaries,
773
+ ownershipRollup: rollup(allSkillFiles, "ownerClass"),
774
+ syncPolicyRollup: rollup(allSkillFiles, "syncPolicy"),
775
+ provenanceRollup: rollup(allSkillFiles, "provenance"),
776
+ dryAudit,
777
+ catalog: {
778
+ path: relHome(catalog.path),
779
+ skills: catalog.skills.map((s) => ({
780
+ name: s.name,
781
+ source: s.source,
782
+ scope: s.scope,
783
+ installed: s.installed,
784
+ description: s.description,
785
+ })),
786
+ },
787
+ missingFromCatalog: missingFromCatalog.map((s) => ({
788
+ name: s.name,
789
+ classes: s.classes,
790
+ ownerClasses: s.ownerClasses,
791
+ provenances: s.provenances,
792
+ syncPolicies: s.syncPolicies,
793
+ samplePaths: s.paths.slice(0, 6),
794
+ })),
795
+ catalogNotFoundInFilesystem: catalogNotFoundInFs.map((s) => s.name),
796
+ uniqueSkills,
797
+ promptContext,
798
+ projectSignals,
799
+ symlinkStatus: linkStatus(),
800
+ machineReport: machineReport ? {
801
+ status: machineReport.status || machineReport.overallStatus || null,
802
+ host: machineReport.host || machineReport.hostname || null,
803
+ generatedAt: machineReport.generatedAt || machineReport.createdAt || null,
804
+ totals: machineReport.totals || null,
805
+ secretValuesExposed: machineReport.secretValuesExposed === true,
806
+ } : null,
807
+ phaseTimings,
808
+ walkIssues,
809
+ notes: [
810
+ "Secret-safe inventory: file paths, filenames, counts, and symlink metadata only.",
811
+ "youmd skill list reads ~/.youmd/skills/youmd-skills.yaml merged with CLI defaultSkills(); it does not crawl every host/global skill root.",
812
+ "Top-level host entry counts and nested SKILL.md counts intentionally differ because stack roots such as gstack and scistack contain their own nested skills.",
813
+ "DRY audit rows are review queues. The scanner never recommends deleting Houston-owned skills automatically.",
814
+ "If walkIssues is non-empty, the inventory is intentionally partial for the listed roots to keep install-time scans bounded.",
815
+ ],
816
+ };
817
+
818
+ function esc(value) {
819
+ return String(value ?? "")
820
+ .replace(/&/g, "&amp;")
821
+ .replace(/</g, "&lt;")
822
+ .replace(/>/g, "&gt;")
823
+ .replace(/"/g, "&quot;");
824
+ }
825
+
826
+ function numberCard(label, value, detail = "") {
827
+ return `<div class="metric"><span>${esc(label)}</span><strong>${esc(value)}</strong><small>${esc(detail)}</small></div>`;
828
+ }
829
+
830
+ function table(headers, rows) {
831
+ return `<table><thead><tr>${headers.map((h) => `<th>${esc(h)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
832
+ }
833
+
834
+ const mermaid = `flowchart TB
835
+ subgraph Canonical["Canonical sources"]
836
+ A["~/.agent-shared\\nAGENTS.md, preferences, STACK-MAP\\n${summary.hostSummaries.find((h) => h.label === "Shared canonical")?.skillFiles || 0} shared SKILL.md"]
837
+ S["~/.claude/scistack\\nHubStack + AstroStack + opt-in extensions\\n${summary.hostSummaries.find((h) => h.label === "SciStack canonical")?.skillFiles || 0} SKILL.md"]
838
+ G["~/.claude/skills/gstack\\nGStack reference stack\\n${summary.hostSummaries.find((h) => h.label === "GStack root")?.skillFiles || 0} SKILL.md"]
839
+ Y["~/.youmd\\nidentity, preferences, directives, private notes\\n${summary.totals.youmdCatalogSkills} catalog skills"]
840
+ end
841
+ subgraph Hosts["Agent exposure hosts"]
842
+ C["Claude Code\\n~/.claude/skills\\n${summary.hostSummaries.find((h) => h.label === "Claude host")?.directEntries || 0} top-level entries"]
843
+ X["Codex\\n~/.codex/skills\\n${summary.hostSummaries.find((h) => h.label === "Codex host")?.directEntries || 0} top-level entries"]
844
+ D["Codex app\\n~/.Codex/skills\\n${summary.hostSummaries.find((h) => h.label === "Codex app host")?.directEntries || 0} top-level entries"]
845
+ R["Cursor\\n~/.cursorrules + ~/.cursor plans/projects\\n${summary.promptContext.cursorPlans} plan prompts"]
846
+ P["Pi / .agents\\n~/.agents/skills\\n${summary.hostSummaries.find((h) => h.label === ".agents host")?.directEntries || 0} top-level entries"]
847
+ end
848
+ subgraph Youmd["You.md sync layer"]
849
+ L["youmd skill list\\n${summary.totals.youmdCatalogSkills} cataloged skills"]
850
+ M["resident daemons\\nidentity + skillstack + project-context"]
851
+ B["You.md API/MCP/account sync\\nremote is ${esc(summary.versions.installedYoumd || "unknown")} local CLI"]
852
+ end
853
+ subgraph Analysis["Inventory intelligence"]
854
+ O["Ownership rollup\\nowned vs external vs plugin"]
855
+ Q["DRY audit\\nreview queues, no auto-delete"]
856
+ V["Future app view\\nConvex + username-you-md repo"]
857
+ end
858
+ A --> C
859
+ A --> X
860
+ A --> R
861
+ A --> P
862
+ S --> C
863
+ S --> X
864
+ G --> C
865
+ G --> X
866
+ Y --> L
867
+ L --> B
868
+ M --> B
869
+ C -.not fully cataloged.-> L
870
+ X -.not fully cataloged.-> L
871
+ C --> O
872
+ X --> O
873
+ O --> Q
874
+ Q --> V`;
875
+
876
+ const hostRows = summary.hostSummaries.map((h) => [
877
+ esc(h.label),
878
+ `<code>${esc(h.root)}</code>`,
879
+ esc(h.directEntries),
880
+ esc(h.directSkillEntries),
881
+ esc(h.skillFiles),
882
+ esc(h.symlinks),
883
+ esc(h.brokenSymlinks),
884
+ `<code>${esc(Object.entries(h.sourceClasses).map(([k, v]) => `${k}:${v}`).join(", ") || "-")}</code>`,
885
+ `<code>${esc(Object.entries(h.ownerClasses).map(([k, v]) => `${k}:${v}`).join(", ") || "-")}</code>`,
886
+ `<code>${esc(Object.entries(h.syncPolicies).map(([k, v]) => `${k}:${v}`).join(", ") || "-")}</code>`,
887
+ ]);
888
+
889
+ const catalogRows = summary.catalog.skills.map((s) => [
890
+ `<code>${esc(s.name)}</code>`,
891
+ esc(s.installed ? "installed" : "available"),
892
+ esc(s.scope),
893
+ `<code>${esc(s.source)}</code>`,
894
+ esc(s.description),
895
+ ]);
896
+
897
+ const missingRows = summary.missingFromCatalog.slice(0, 400).map((s) => [
898
+ `<code>${esc(s.name)}</code>`,
899
+ `<code>${esc(s.classes.join(", "))}</code>`,
900
+ `<code>${esc(s.ownerClasses.join(", "))}</code>`,
901
+ `<code>${esc(s.syncPolicies.join(", "))}</code>`,
902
+ `<code>${esc(s.samplePaths.join("\\n"))}</code>`,
903
+ ]);
904
+
905
+ const rollupRows = Object.entries(summary.ownershipRollup)
906
+ .sort((a, b) => b[1] - a[1])
907
+ .map(([key, value]) => [esc(key), esc(value)]);
908
+
909
+ const policyRows = Object.entries(summary.syncPolicyRollup)
910
+ .sort((a, b) => b[1] - a[1])
911
+ .map(([key, value]) => [esc(key), esc(value)]);
912
+
913
+ const duplicateRows = summary.dryAudit.duplicateNameDifferentRealpaths.slice(0, 160).map((s) => [
914
+ `<code>${esc(s.name)}</code>`,
915
+ esc(s.realpathCount),
916
+ esc(s.occurrences),
917
+ `<code>${esc(s.owners.join(", "))}</code>`,
918
+ `<code>${esc(s.risk)}</code>`,
919
+ `<code>${esc(s.samplePaths.join("\\n"))}</code>`,
920
+ ]);
921
+
922
+ const mirrorRows = summary.dryAudit.sameRealpathMirrors.slice(0, 160).map((s) => [
923
+ `<code>${esc(s.name)}</code>`,
924
+ esc(s.occurrences),
925
+ `<code>${esc(s.owners.join(", "))}</code>`,
926
+ `<code>${esc(s.samplePaths.join("\\n"))}</code>`,
927
+ ]);
928
+
929
+ const linkRows = summary.symlinkStatus.map((s) => [
930
+ `<code>${esc(s.link)}</code>`,
931
+ esc(s.kind),
932
+ esc(s.ok ? "ok" : "check"),
933
+ `<code>${esc(s.actual || "-")}</code>`,
934
+ `<code>${esc(s.target)}</code>`,
935
+ ]);
936
+
937
+ const phaseRows = summary.phaseTimings.map((s) => [
938
+ `<code>${esc(s.phase)}</code>`,
939
+ esc(s.label),
940
+ esc(Math.round((s.durationMs || 0) / 1000)),
941
+ esc(s.failed ? "failed" : "ok"),
942
+ ]);
943
+
944
+ const promptRows = Object.entries(summary.promptContext).map(([key, value]) => [
945
+ esc(key),
946
+ typeof value === "object" ? `<code>${esc(JSON.stringify(value))}</code>` : esc(value),
947
+ ]);
948
+
949
+ const html = `<!doctype html>
950
+ <html lang="en">
951
+ <head>
952
+ <meta charset="utf-8" />
953
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
954
+ <title>You.md Local Agent Stack Inventory</title>
955
+ <script type="module">
956
+ import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
957
+ mermaid.initialize({ startOnLoad: true, theme: "dark", securityLevel: "loose" });
958
+ </script>
959
+ <style>
960
+ :root {
961
+ color-scheme: dark;
962
+ --bg: #0d0d0d;
963
+ --panel: #151312;
964
+ --line: #37312d;
965
+ --text: #f2eee9;
966
+ --muted: #a69b93;
967
+ --accent: #c46a3a;
968
+ --ok: #6fbf73;
969
+ --warn: #d6a84f;
970
+ }
971
+ * { box-sizing: border-box; }
972
+ body {
973
+ margin: 0;
974
+ background: var(--bg);
975
+ color: var(--text);
976
+ font: 14px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
977
+ }
978
+ header, main { max-width: 1240px; margin: 0 auto; padding: 28px; }
979
+ header { border-bottom: 1px solid var(--line); }
980
+ h1, h2, h3, code, th, .metric strong { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; }
981
+ h1 { margin: 0 0 10px; font-size: clamp(26px, 4vw, 44px); font-weight: 500; letter-spacing: 0; }
982
+ h2 { margin: 42px 0 14px; font-size: 19px; font-weight: 500; color: var(--accent); }
983
+ h3 { margin: 26px 0 10px; font-size: 15px; font-weight: 500; color: var(--text); }
984
+ p { color: var(--muted); max-width: 920px; }
985
+ a { color: var(--accent); }
986
+ code {
987
+ color: #f6d0bd;
988
+ white-space: pre-wrap;
989
+ overflow-wrap: anywhere;
990
+ font-size: 12px;
991
+ }
992
+ .grid {
993
+ display: grid;
994
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
995
+ gap: 10px;
996
+ margin: 22px 0;
997
+ }
998
+ .metric {
999
+ border: 1px solid var(--line);
1000
+ background: var(--panel);
1001
+ border-radius: 2px;
1002
+ padding: 14px;
1003
+ min-height: 108px;
1004
+ }
1005
+ .metric span, .metric small { display: block; color: var(--muted); }
1006
+ .metric strong { display: block; margin: 8px 0 3px; font-size: 28px; font-weight: 500; color: var(--text); }
1007
+ .callout {
1008
+ border-left: 2px solid var(--accent);
1009
+ background: #17110e;
1010
+ padding: 14px 16px;
1011
+ color: var(--muted);
1012
+ margin: 18px 0;
1013
+ }
1014
+ .diagram {
1015
+ border: 1px solid var(--line);
1016
+ background: #111;
1017
+ border-radius: 2px;
1018
+ padding: 14px;
1019
+ overflow-x: auto;
1020
+ }
1021
+ .two-col {
1022
+ display: grid;
1023
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
1024
+ gap: 14px;
1025
+ align-items: start;
1026
+ }
1027
+ table {
1028
+ width: 100%;
1029
+ border-collapse: collapse;
1030
+ border: 1px solid var(--line);
1031
+ margin: 12px 0 26px;
1032
+ background: var(--panel);
1033
+ }
1034
+ th, td {
1035
+ border-bottom: 1px solid var(--line);
1036
+ padding: 9px 10px;
1037
+ text-align: left;
1038
+ vertical-align: top;
1039
+ }
1040
+ th {
1041
+ color: var(--muted);
1042
+ font-size: 11px;
1043
+ text-transform: uppercase;
1044
+ letter-spacing: 0;
1045
+ font-weight: 500;
1046
+ }
1047
+ tr:last-child td { border-bottom: 0; }
1048
+ details {
1049
+ border: 1px solid var(--line);
1050
+ border-radius: 2px;
1051
+ background: var(--panel);
1052
+ padding: 12px 14px;
1053
+ margin: 12px 0;
1054
+ }
1055
+ summary { cursor: pointer; color: var(--accent); font-family: "JetBrains Mono", ui-monospace, monospace; }
1056
+ .status-ok { color: var(--ok); }
1057
+ .status-warn { color: var(--warn); }
1058
+ </style>
1059
+ </head>
1060
+ <body>
1061
+ <header>
1062
+ <h1>You.md Local Agent Stack Inventory</h1>
1063
+ <p>Generated ${esc(summary.generatedAt)} on <code>${esc(summary.host)}</code>. Secret-safe snapshot of local/global skills, host exposure roots, shared prompts/preferences, project context, logs, and You.md catalog coverage.</p>
1064
+ <p>Companion JSON: <code>${esc(relHome(outJson))}</code></p>
1065
+ </header>
1066
+ <main>
1067
+ <section class="grid">
1068
+ ${numberCard("Unique skill names found", summary.totals.uniqueSkillNames, "across catalog + host + stack roots")}
1069
+ ${numberCard("Unique real SKILL.md files", summary.totals.uniqueRealSkillFiles, "deduped by realpath")}
1070
+ ${numberCard("Exposure + canonical records", summary.totals.skillFileOccurrences, `${summary.totals.directExposureSkillRecords} host exposures + ${summary.totals.canonicalSkillFiles} canonical files`)}
1071
+ ${numberCard("You.md catalog", `${summary.totals.youmdCatalogInstalled}/${summary.totals.youmdCatalogSkills}`, "installed / cataloged")}
1072
+ ${numberCard("Missing from catalog", summary.totals.missingFromYoumdCatalog, "filesystem skills not represented in youmd skill list")}
1073
+ ${numberCard("Duplicate-name risks", summary.totals.duplicateNameDifferentRealpaths, "same name, different real files")}
1074
+ ${numberCard("Healthy mirrors", summary.totals.sameRealpathMirrors, "same real file exposed to hosts")}
1075
+ ${numberCard("Project context signals", summary.totals.projectSignals, "AGENTS/CLAUDE/project-context/YouStack")}
1076
+ ${numberCard("Installed CLI", summary.versions.installedYoumd || "unknown", `repo ${summary.versions.repoPackage || "unknown"} / cli ${summary.versions.cliPackage || "unknown"}`)}
1077
+ </section>
1078
+
1079
+ <div class="callout">
1080
+ <strong>Initial read:</strong> <code>youmd skill list</code> is not a full local skill inventory today. It reads <code>~/.youmd/skills/youmd-skills.yaml</code> plus the CLI's hard-coded default skills. The real machine has many more local/global skills exposed through Claude, Codex, GStack, SciStack, shared-agent symlinks, and plugin caches. Counts separate direct host exposure from canonical nested stack files so symlinks do not blur the map.
1081
+ </div>
1082
+
1083
+ <h2>Topology</h2>
1084
+ <div class="diagram"><pre class="mermaid">${esc(mermaid)}</pre></div>
1085
+
1086
+ <h2>Sync Roots</h2>
1087
+ ${table(["Root", "Path", "Top-level entries", "Direct skill entries", "Nested SKILL.md", "Symlinks", "Broken symlinks", "Source classes", "Owner classes", "Sync policies"], hostRows)}
1088
+
1089
+ <h2>Ownership</h2>
1090
+ <p>Owned skills should be protected and made canonical; external/reference/plugin skills should be cataloged with provenance instead of copied into the personal stack blindly.</p>
1091
+ <div class="two-col">
1092
+ <div>${table(["Owner class", "Records"], rollupRows)}</div>
1093
+ <div>${table(["Sync policy", "Records"], policyRows)}</div>
1094
+ </div>
1095
+
1096
+ <h2>DRY Audit</h2>
1097
+ <div class="callout">
1098
+ These are review queues, not destructive instructions. Same-realpath mirrors usually mean healthy host exposure. Same-name/different-realpath rows need a human/agent decision, and Houston-owned skills win priority over public or plugin versions.
1099
+ </div>
1100
+ <h3>Same Name, Different Real Files</h3>
1101
+ ${table(["Skill", "Real files", "Occurrences", "Owners", "Risk", "Sample paths"], duplicateRows)}
1102
+ <h3>Same Real File Mirrored Across Hosts</h3>
1103
+ ${table(["Skill", "Occurrences", "Owners", "Sample paths"], mirrorRows)}
1104
+
1105
+ <h2>You.md Catalog</h2>
1106
+ <p>This is what <code>youmd skill list</code> sees today.</p>
1107
+ ${table(["Skill", "State", "Scope", "Source", "Description"], catalogRows)}
1108
+
1109
+ <h2>Catalog Gap</h2>
1110
+ <p>Filesystem skills not currently represented in the You.md skill catalog. Showing up to 400 rows; full data is in JSON.</p>
1111
+ ${table(["Skill", "Classes", "Owners", "Sync policies", "Sample paths"], missingRows)}
1112
+
1113
+ <h2>Shared Instructions Symlinks</h2>
1114
+ ${table(["Link", "Kind", "Status", "Actual target", "Expected target"], linkRows)}
1115
+
1116
+ <h2>Prompts, Preferences, Memory, Logs</h2>
1117
+ ${table(["Bucket", "Count / summary"], promptRows)}
1118
+
1119
+ <h2>Project Context Coverage</h2>
1120
+ <p>Workspace scan under <code>${esc(relHome(roots.workspace))}</code>: ${esc(JSON.stringify(summary.projectSignals.buckets))}</p>
1121
+ <details>
1122
+ <summary>Show sampled project context paths</summary>
1123
+ <pre><code>${esc(summary.projectSignals.sample.join("\\n"))}</code></pre>
1124
+ </details>
1125
+
1126
+ <h2>Machine Proof</h2>
1127
+ <pre><code>${esc(JSON.stringify(summary.machineReport, null, 2))}</code></pre>
1128
+
1129
+ <h2>Traversal Guardrails</h2>
1130
+ <p>Install-time scans are bounded so one huge local tree cannot hang machine setup.</p>
1131
+ ${table(["Phase", "Label", "Seconds", "Status"], phaseRows)}
1132
+ <pre><code>${esc(JSON.stringify(summary.walkIssues, null, 2))}</code></pre>
1133
+
1134
+ <h2>Notes</h2>
1135
+ <ul>
1136
+ ${summary.notes.map((note) => `<li>${esc(note)}</li>`).join("")}
1137
+ </ul>
1138
+ </main>
1139
+ </body>
1140
+ </html>`;
1141
+
1142
+ fs.writeFileSync(outJson, JSON.stringify(summary, null, 2) + "\n");
1143
+ fs.writeFileSync(outHtml, html);
1144
+
1145
+ console.log(JSON.stringify({
1146
+ html: outHtml,
1147
+ json: outJson,
1148
+ totals: summary.totals,
1149
+ installedYoumdVersion: summary.versions.installedYoumd,
1150
+ }, null, 2));