[object Object]

← 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 →