← back to Stack Map Viewer
stack-map: add live folders + drill-downs for skills, agents, projects
da88d747921ce687e87e5d12566ccfeaa44a1c41 · 2026-09-22 11:22:56 -0700 · Steve Abrams
- scan ~/Projects (768 dirs, 741 git repos) as a new drillable Projects node
- 31 name-regex folders: 15 skills, 8 agents, 8 projects — each a live drill-down
- make Agents + Skills SVG nodes clickable (were not); add pj_all/pj_repos
- grouped 'Folders — click to drill' chip strip below the map
- filter dot-dirs from scans; trim _lists from the 30s stats poll payload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8csQCEvLbTkcFz5CRqXq
Files touched
Diff
commit da88d747921ce687e87e5d12566ccfeaa44a1c41
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 22 11:22:56 2026 -0700
stack-map: add live folders + drill-downs for skills, agents, projects
- scan ~/Projects (768 dirs, 741 git repos) as a new drillable Projects node
- 31 name-regex folders: 15 skills, 8 agents, 8 projects — each a live drill-down
- make Agents + Skills SVG nodes clickable (were not); add pj_all/pj_repos
- grouped 'Folders — click to drill' chip strip below the map
- filter dot-dirs from scans; trim _lists from the 30s stats poll payload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8csQCEvLbTkcFz5CRqXq
---
index.html | 45 +++++++++++++++++++++++---
server.js | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------
2 files changed, 134 insertions(+), 19 deletions(-)
diff --git a/index.html b/index.html
index f0fe56f..236ae14 100644
--- a/index.html
+++ b/index.html
@@ -26,6 +26,16 @@
#drawer li:nth-child(odd){background:#181c28}
#close{float:right;cursor:pointer;color:var(--dim);font-size:20px;line-height:1}
.foot{color:var(--dim);font-size:12px;padding:8px 20px}
+ #folders{padding:4px 20px 30px}
+ .fbar{color:var(--dim);font-size:12px;margin:2px 0 10px;border-top:1px solid #222a3a;padding-top:12px}
+ .fgroup{margin-bottom:14px}
+ .ghead{font-size:13px;font-weight:700;color:#9fc4ff;letter-spacing:.4px;margin:6px 0 8px}
+ .ghead .gn{color:#6f7d97;font-weight:400;font-size:11px;margin-left:6px}
+ .chips{display:flex;flex-wrap:wrap;gap:8px}
+ .chip{display:inline-flex;align-items:center;gap:8px;background:#181c28;border:1px solid #39507a;border-radius:8px;
+ color:#c7d2e6;font-size:12px;padding:6px 10px;cursor:pointer;font-family:inherit;transition:background .12s}
+ .chip:hover{background:#20304d}
+ .chip .cn{background:#20304d;color:#8fe3b4;border-radius:10px;padding:1px 8px;font-weight:700;font-size:11px}
</style></head><body>
<header>
<h1>Steve's Stack — Interconnection Map</h1>
@@ -33,6 +43,7 @@
<span class="badge UNKNOWN" id="verdict">…</span>
</header>
<div id="wrap"><div id="svgbox"></div></div>
+<div id="folders"></div>
<div class="foot" id="foot"></div>
<div id="drawer"><span id="close" onclick="document.getElementById('drawer').classList.remove('open')">✕</span>
<h2 id="dtitle"></h2><div class="cnt" id="dcnt"></div><ul id="dlist"></ul></div>
@@ -60,15 +71,15 @@ function render(d){
${txt(56,340,'route + final sign-off on gated work — click to list','#6f7d97',11,'font-style="italic"')}
<line x1="170" y1="186" x2="230" y2="238" stroke="#7d8aa5" stroke-width="2" marker-end="url(#arw)"/>
- ${box(40,406,480,120,'#1c2740','#3f5a86')}
- ${txt(56,432,`~${N(d.agents-d.officers)} WORKER AGENTS (${N(d.agents)} total)`,'#9fc4ff',16,'font-weight="700"')}
+ ${box(40,406,480,120,'#1c2740','#3f5a86','ag_workers')}
+ ${txt(56,432,`~${N(d.agents-d.officers)} WORKER AGENTS (${N(d.agents)} total) ▸`,'#9fc4ff',16,'font-weight="700"')}
${txt(56,456,'vendor stewards · site/build · marketing · directory builders','#c7d2e6',12)}
${txt(56,476,'contrarian · approval-agent · fleet-lifecycle · masterdot · google-merchant','#c7d2e6',12)}
${txt(56,500,'own & supervise; recruit subagents (plan→split→argue→merge)','#6f7d97',11,'font-style="italic"')}
<line x1="280" y1="356" x2="280" y2="404" stroke="#7d8aa5" stroke-width="2" marker-end="url(#arw)"/>
- ${box(40,576,480,214,'#182135','#39507a')}
- ${txt(56,602,`${N(d.skills)} SKILLS (invoked via Skill tool)`,'#9fc4ff',16,'font-weight="700"')}
+ ${box(40,576,480,214,'#182135','#39507a','sk_all')}
+ ${txt(56,602,`${N(d.skills)} SKILLS (invoked via Skill tool) ▸`,'#9fc4ff',16,'font-weight="700"')}
${box(56,616,200,34,'#20304d','#20304d','canaries')}${txt(66,637,`${N(d.canaries)} canaries / monitors`,'#c7d2e6',12)}
${box(266,616,238,34,'#20304d','#20304d','scrapers')}${txt(276,637,`${N(d.scrapers)} vendor scraper-managers`,'#c7d2e6',12)}
${box(56,658,200,34,'#20304d','#20304d','dots')}${txt(66,679,`${N(d.dots)} terminal dot/color`,'#c7d2e6',12)}
@@ -124,6 +135,14 @@ function render(d){
${txt(966,238,`painted by ${N(d.dots)} dot skills + masterdot + router`,'#7f88ad',11,'font-style="italic"')}
<path d="M300,585 C620,560 820,360 948,240" fill="none" stroke="#3a4761" stroke-width="1.6" stroke-dasharray="5 4" marker-end="url(#arw)"/>
+ ${box(950,300,290,150,'#2a2136','#6b4f86','pj_all')}
+ ${txt(966,328,`${N(d.projects)} PROJECTS (~/Projects) ▸`,'#d6b8ff',15,'font-weight="700"')}
+ ${box(966,344,120,30,'#3a2f52','#3a2f52','pj_repos')}${txt(976,364,`${N(d.projectRepos)} git repos`,'#cfc3e0',12)}
+ ${txt(966,398,'DW · scrapers · websites · directories','#cfc3e0',12)}
+ ${txt(966,418,'viewers · media · infra — click to drill','#8f7fb0',11,'font-style="italic"')}
+ ${txt(966,440,'each build is a git repo (standing rule)','#8f7fb0',11,'font-style="italic"')}
+ <line x1="520" y1="466" x2="948" y2="400" stroke="#3a4761" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#arw)"/>
+
${box(1270,120,370,700,'#12151f','#2a3247')}
${txt(1288,150,'Reading the map','#eef1f7',17,'font-weight="700"')}
${txt(1288,184,'Four control planes tie it together:','#c7d2e6',13)}
@@ -142,11 +161,27 @@ function render(d){
${txt(1288,614,`Live scan · updated ${new Date(d.ts).toLocaleTimeString()}`,'#6f7d97',11,'font-style="italic"')}
</svg>`;
document.getElementById('svgbox').innerHTML=s;
- document.getElementById('sub').textContent=`${N(d.skills)} skills · ${N(d.agents)} agents · ${N(d.cronLoaded)} cron · ${N(d.parked)} parked · ${N(d.ledger)} reversible actions`;
+ document.getElementById('sub').textContent=`${N(d.skills)} skills · ${N(d.agents)} agents · ${N(d.projects)} projects · ${N(d.cronLoaded)} cron · ${N(d.parked)} parked`;
+ renderFolders(d.folders||[]);
const vb=document.getElementById('verdict'); vb.textContent='fleet: '+d.verdict; vb.className='badge '+d.verdict;
document.getElementById('foot').textContent=`Live from ~/.claude · ~/Library/LaunchAgents · yolo-queue — auto-refresh 30s — last scan ${new Date(d.ts).toLocaleTimeString()}`;
document.querySelectorAll('rect.node[data-kind]').forEach(r=>r.addEventListener('click',()=>drill(r.dataset.kind)));
}
+function renderFolders(folders){
+ const el=document.getElementById('folders');
+ if(!folders.length){el.innerHTML='';return;}
+ const groups={}; folders.forEach(f=>{(groups[f.group]=groups[f.group]||[]).push(f)});
+ const order=['SKILLS','AGENTS','PROJECTS'];
+ const gcolor={SKILLS:'#39507a',AGENTS:'#3f5a86',PROJECTS:'#6b4f86'};
+ let html='<div class="fbar">Folders — click any to drill into its members (live-scanned)</div>';
+ order.forEach(g=>{ const arr=groups[g]; if(!arr)return;
+ const tot=arr.reduce((a,f)=>a+f.n,0);
+ html+=`<div class="fgroup"><div class="ghead">${g}<span class="gn">${arr.length} folders · ${N(tot)} members</span></div><div class="chips">`+
+ arr.map(f=>`<button class="chip" style="border-color:${gcolor[g]||'#39507a'}" onclick="drill('${f.kind}')" title="${f.label}"><span class="cl">${f.label}</span><span class="cn">${N(f.n)}</span></button>`).join('')+
+ `</div></div>`;
+ });
+ el.innerHTML=html;
+}
async function drill(kind){
const r=await fetch('/api/list?kind='+kind); const j=await r.json();
document.getElementById('dtitle').textContent=kind;
diff --git a/server.js b/server.js
index df4923b..4ecfd5f 100644
--- a/server.js
+++ b/server.js
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Stack Interconnection Map — live web viewer
-// Basic-auth admin/DW2024! · live-scans ~/.claude, ~/Library/LaunchAgents, yolo-queue
+// Basic-auth admin/DW2024! · live-scans ~/.claude, ~/Library/LaunchAgents, yolo-queue, ~/Projects
const http = require('http');
const { execSync } = require('child_process');
const fs = require('fs');
@@ -10,47 +10,123 @@ const path = require('path');
const HOME = os.homedir();
const SKILLS = path.join(HOME, '.claude/skills');
const AGENTS = path.join(HOME, '.claude/agents');
+const PROJECTS = path.join(HOME, 'Projects');
const LA = path.join(HOME, 'Library/LaunchAgents');
const PENDING = path.join(HOME, '.claude/yolo-queue/pending-approval');
const LEDGER = path.join(HOME, '.claude/yolo-queue/executed-reversible/ledger.jsonl');
const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
const sh = (c) => { try { return execSync(c, { encoding: 'utf8', timeout: 15000 }); } catch { return ''; } };
-const lsdirs = (p, re) => { try { return fs.readdirSync(p, { withFileTypes: true }).filter(d => d.isDirectory() && (!re || re.test(d.name))).map(d => d.name); } catch { return []; } };
+const lsdirs = (p, re) => { try { return fs.readdirSync(p, { withFileTypes: true }).filter(d => d.isDirectory() && !d.name.startsWith('.') && (!re || re.test(d.name))).map(d => d.name); } catch { return []; } };
+const bucket = (names, re) => names.filter(n => re.test(n)).sort();
+
+// ── Folder taxonomy (name-regex buckets) ────────────────────────────────────
+// Each entry: [drill-kind, display label, regex]. Drill-kind must be unique.
+// Adding a row here creates a live folder + drill-down with zero other edits.
+const SKILL_CATS = [
+ ['sk_canary', 'canaries / monitors', /canary/],
+ ['sk_scraper', 'vendor scraper-managers', /scraper-manager$|-scraper$/],
+ ['sk_dots', 'terminal dots / color', /dot|^color$|^flash$|green|orange|purple|yellow|pink|lightblue/],
+ ['sk_video', 'hyperframes / video', /hyperframes|video|reel|avatar|heygen|clip|caption|motion|film|slideshow/],
+ ['sk_social', 'social / marketing', /instagram|tiktok|linkedin|facebook|pinterest|youtube|social|marketing|^dw-x|kartiseira|threads|promo/],
+ ['sk_pricing', 'pricing / cost', /price|cost|-map-|margin|kravet|discount|pricing/],
+ ['sk_settlement', 'settlement gate', /settlement/],
+ ['sk_storefront', 'storefront / site UI', /site|storefront|landing|hero|grid|viewer|rotator|bento|flipbook|gallery|page-flip|kv-list|modal|nav|scroll/],
+ ['sk_research', 'research / web', /research|web-|websearch|last30|deep|exa|knowledge|scout|competitor|peer-survey/],
+ ['sk_orchestration','loops / orchestration', /yolo|loop|officer|dtd|contrarian|council|plannator|ralph|masterdot|ticketmaster|agent-team|coordinat|abramstasks|iterm/],
+ ['sk_shopify', 'shopify / catalog', /shopify|catalog|sku|metafield|gmc|merchant|activator|inventory|vendor-|reconcile/],
+ ['sk_imagedesign', 'image / design', /image|design|room|mockup|logo|seam|edges|luxe|four-horsemen|graphic|interior|elements|fix-|regen/],
+ ['sk_health', 'health / fleet guards', /health|rollup|fleet|drift|watchdog|sentinel|uptime|-guard|liveness/],
+ ['sk_email', 'email / comms', /email|george|mailer|gmail|purelymail|comms|newsletter|inbox|reply/],
+ ['sk_infra', 'infra / deploy', /domain|dns|cloudflare|nginx|kamatera|deploy|firewall|secret|backup|disk|cron|pm2|egress/],
+];
+
+const AGENT_CATS = [
+ ['ag_officers', 'cabinet officers (vp-*)', /^vp-/],
+ ['ag_vendor', 'DW commerce / vendor', /^dw-(?!x|instagram|tiktok|facebook|linkedin|pinterest|youtube|websites|promo|marketing)|scraper|vendor|shopify|catalog|greenland|fentucci|momentum|wallquest|google-merchant|showroom|fineartamerica|sister-parish/],
+ ['ag_marketing', 'marketing / social', /marketing|instagram|tiktok|linkedin|facebook|pinterest|youtube|dw-x|dw-promo|dw-websites|front-page|graphic|ui-ux|seo|reels|logo/],
+ ['ag_engineering', 'engineering', /engineer|review|architect|debug|error|test|security|database|backend|frontend|fullstack|python|typescript|javascript|mobile|ios|performance|code|prompt|mcp|api-doc|refactor/],
+ ['ag_directory', 'directories', /directory|doctor|lawyer|la-research|adsense|domain|professional/],
+ ['ag_research', 'research / data', /research|web-researcher|search|technical|data-|product-strategist|analyst|scientist|seo-analy|task-decomp|context-manager/],
+ ['ag_special', 'special projects', /wallpapersback|apartment|seam|consulting|abramsego|bounce|wpb|garcia|elevator|avatar/],
+ ['ag_ops', 'ops / approval / fleet', /approval|cncp|operations|fleet|kamatera|pm2|masterdot|contrarian|deployment|devops|statusline/],
+];
+
+const PROJECT_CATS = [
+ ['pj_dw', 'DW / designerwallcoverings',/^dw-|designerwallcoverings|-internal$|shopify|catalog|vendor|filemaker/i],
+ ['pj_scraper', 'scrapers / crawlers', /scraper|crawl|refresh|-catalog|reprice|onboard|exec$/i],
+ ['pj_website', 'websites / storefronts', /wallpaper|wallcovering|grasscloth|flocked|novasuede|silk|linen|cork|storefront|microsite|\.com|website|site$/i],
+ ['pj_canary', 'canaries / monitors', /canary|monitor|watchdog|guard|health|sentinel/i],
+ ['pj_viewer', 'viewers / dashboards', /viewer|dashboard|-board|map$|panel|console|admin/i],
+ ['pj_directory', 'directories / real-estate',/directory|lawyer|doctor|calbar|animals|ventura|crcp|realestate|homesonspec|commercialreal|lacounty/i],
+ ['pj_media', 'media / video / photo', /video|reel|hyperframes|photo|image|render|comfy|design|mockup/i],
+ ['pj_infra', 'infra / agents / tools', /agent|ticket|secret|domain|george|gmail|nas|cluster|exo|mcp|cncp|tool/i],
+];
let CACHE = null, CACHE_TS = 0;
function scan() {
if (CACHE && Date.now() - CACHE_TS < 20000) return CACHE;
const skillDirs = lsdirs(SKILLS);
- const canaries = skillDirs.filter(n => /canary/.test(n));
- const scrapers = skillDirs.filter(n => /scraper-manager$/.test(n));
- const dots = skillDirs.filter(n => /dot|^color$|green|orange|purple|yellow|pink|lightblue/.test(n));
- const video = skillDirs.filter(n => /hyperframes|video/.test(n));
+ const canaries = bucket(skillDirs, /canary/);
+ const scrapers = bucket(skillDirs, /scraper-manager$/);
+ const dots = bucket(skillDirs, /dot|^color$|green|orange|purple|yellow|pink|lightblue/);
+ const video = bucket(skillDirs, /hyperframes|video/);
const health = (sh(`ls ${SKILLS}/*/data/latest.json 2>/dev/null | wc -l`).trim() || '0');
+
const agentFiles = (() => { try { return fs.readdirSync(AGENTS).filter(f => f.endsWith('.md')); } catch { return []; } })();
- const officers = agentFiles.filter(f => f.startsWith('vp-'));
+ const agentNames = agentFiles.map(f => f.replace('.md', ''));
+ const officers = agentNames.filter(n => n.startsWith('vp-')).sort();
+ const workers = agentNames.filter(n => !n.startsWith('vp-')).sort();
+
+ const projectDirs = lsdirs(PROJECTS).sort();
+ const projectRepos = projectDirs.filter(d => { try { return fs.existsSync(path.join(PROJECTS, d, '.git')); } catch { return false; } });
+
const cronLoaded = (sh(`launchctl list 2>/dev/null | grep -c com.steve`).trim() || '0');
const cronPlists = (() => { try { return fs.readdirSync(LA).filter(f => /^com\.steve\..*\.plist$/.test(f)); } catch { return []; } })();
- // cron -> skill edges
let cronToSkill = 0;
for (const f of cronPlists) { const t = sh(`grep -oE '/skills/[^/< ]+' '${path.join(LA, f)}' 2>/dev/null | head -1`).trim(); if (t) cronToSkill++; }
- // parked + age buckets
+
let parked = [], p3 = 0, p24 = 0, p7d = 0; const now = Date.now();
try { parked = fs.readdirSync(PENDING).filter(f => f.endsWith('.md')); } catch {}
for (const f of parked) { try { const m = fs.statSync(path.join(PENDING, f)).mtimeMs; const h = (now - m) / 3.6e6; if (h >= 3) p3++; if (h >= 24) p24++; if (h >= 168) p7d++; } catch {} }
const ledger = (sh(`wc -l < ${LEDGER} 2>/dev/null`).trim() || '0');
- // live fleet-health verdict
+
let verdict = 'UNKNOWN';
try { const j = JSON.parse(fs.readFileSync(path.join(SKILLS, 'fleet-health-rollup/data/latest.json'), 'utf8')); verdict = j.verdict || j.status || 'UNKNOWN'; } catch {}
+ // Build category lists + a live "folders" descriptor array grouped for the UI.
+ const _lists = {
+ canaries, scrapers, dots, video,
+ officers, workers,
+ sk_all: skillDirs.slice().sort(),
+ ag_all: agentNames.slice().sort(),
+ ag_workers: workers,
+ pj_all: projectDirs,
+ pj_repos: projectRepos.slice().sort(),
+ parked: parked.slice().sort(),
+ };
+ const folders = [];
+ const addCats = (group, names, cats) => {
+ for (const [kind, label, re] of cats) {
+ const items = bucket(names, re);
+ _lists[kind] = items;
+ if (items.length) folders.push({ group, kind, label, n: items.length });
+ }
+ };
+ addCats('SKILLS', skillDirs, SKILL_CATS);
+ addCats('AGENTS', agentNames, AGENT_CATS);
+ addCats('PROJECTS', projectDirs, PROJECT_CATS);
+
CACHE = {
ts: new Date().toISOString(),
skills: skillDirs.length, canaries: canaries.length, scrapers: scrapers.length,
dots: dots.length, video: video.length, health: +health,
- agents: agentFiles.length, officers: officers.length,
+ agents: agentFiles.length, officers: officers.length, workers: workers.length,
+ projects: projectDirs.length, projectRepos: projectRepos.length,
cronLoaded: +cronLoaded, cronPlists: cronPlists.length, cronToSkill,
parked: parked.length, p3, p24, p7d, ledger: +ledger, verdict,
- _lists: { canaries, scrapers, dots, video, officers: officers.map(f => f.replace('.md', '')), parked }
+ folders,
+ _lists,
};
CACHE_TS = Date.now();
return CACHE;
@@ -63,11 +139,15 @@ const server = http.createServer((req, res) => {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="stack-map"' }); return res.end('auth required');
}
const u = new URL(req.url, 'http://x');
- if (u.pathname === '/api/stats') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify(scan())); }
+ if (u.pathname === '/api/stats') {
+ const s = scan();
+ const { _lists, ...pub } = s; // don't ship every list on every poll; drill fetches lists on demand
+ res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify(pub));
+ }
if (u.pathname === '/api/list') {
const kind = u.searchParams.get('kind'); const s = scan();
res.writeHead(200, { 'content-type': 'application/json' });
- return res.end(JSON.stringify({ kind, items: (s._lists[kind] || []).sort() }));
+ return res.end(JSON.stringify({ kind, items: (s._lists[kind] || []).slice().sort() }));
}
res.writeHead(200, { 'content-type': 'text/html' }); res.end(PAGE);
});
← c84072e auto-data-snapshot: 2026-09-22T10:38:29 (1 data files) — .po
·
back to Stack Map Viewer
·
viewer: draggable nodes + recursive drill + view switcher a676017 →