@jakkrichm/create-nexus-devflow 2.13.0 → 2.15.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 (65) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/create-nexus-devflow.d.ts +1 -1
  3. package/dist/bin/create-nexus-devflow.js +4 -3
  4. package/dist/bin/create-nexus-devflow.js.map +1 -1
  5. package/dist/lib/branch-context.d.ts +1 -0
  6. package/dist/lib/branch-context.js +8 -3
  7. package/dist/lib/branch-context.js.map +1 -1
  8. package/dist/lib/dashboard-page.d.ts +5 -2
  9. package/dist/lib/dashboard-page.js +7 -388
  10. package/dist/lib/dashboard-page.js.map +1 -1
  11. package/dist/lib/dashboard-snapshot.d.ts +22 -3
  12. package/dist/lib/dashboard-snapshot.js +154 -3
  13. package/dist/lib/dashboard-snapshot.js.map +1 -1
  14. package/dist/lib/delivery-lifecycle-engine.d.ts +1 -1
  15. package/dist/lib/delivery-lifecycle-engine.js +1 -1
  16. package/dist/lib/delivery-lifecycle-engine.js.map +1 -1
  17. package/dist/lib/mcp-server-engine.d.ts +57 -0
  18. package/dist/lib/mcp-server-engine.js +678 -0
  19. package/dist/lib/mcp-server-engine.js.map +1 -0
  20. package/dist/lib/mcp.d.ts +15 -27
  21. package/dist/lib/mcp.js +20 -646
  22. package/dist/lib/mcp.js.map +1 -1
  23. package/dist/lib/project-status-engine.d.ts +110 -0
  24. package/dist/lib/project-status-engine.js +493 -0
  25. package/dist/lib/project-status-engine.js.map +1 -0
  26. package/dist/lib/skill-manager.d.ts +2 -82
  27. package/dist/lib/skill-manager.js +15 -856
  28. package/dist/lib/skill-manager.js.map +1 -1
  29. package/dist/lib/skill-registry-engine.d.ts +121 -0
  30. package/dist/lib/skill-registry-engine.js +824 -0
  31. package/dist/lib/skill-registry-engine.js.map +1 -0
  32. package/dist/lib/status.d.ts +1 -90
  33. package/dist/lib/status.js +7 -467
  34. package/dist/lib/status.js.map +1 -1
  35. package/dist/lib/studio-view-renderer.d.ts +17 -1
  36. package/dist/lib/studio-view-renderer.js +855 -3
  37. package/dist/lib/studio-view-renderer.js.map +1 -1
  38. package/dist/lib/tooling/commands/scan-doc-contract.js +1 -0
  39. package/dist/lib/tooling/commands/scan-doc-contract.js.map +1 -1
  40. package/dist/lib/update.js +1 -1
  41. package/dist/lib/update.js.map +1 -1
  42. package/package.json +1 -1
  43. package/template/.agents/skills/complete/SKILL.md +14 -13
  44. package/template/.agents/skills/implement/SKILL.md +36 -2
  45. package/template/.claude/skills/complete/SKILL.md +14 -13
  46. package/template/.claude/skills/implement/SKILL.md +36 -2
  47. package/template/AGENTS.md +1 -1
  48. package/template/devflow/build-plan.md +5 -4
  49. package/template/devflow/context/ai-interaction.md +4 -2
  50. package/template/devflow/docs/USAGE.md +145 -0
  51. package/template/devflow/docs/diagrams/nexus-devflow-architecture.html +14821 -0
  52. package/template/devflow/docs/diagrams/nexus-devflow-architecture.json +80 -0
  53. package/template/devflow/docs/diagrams/nexus-devflow-lifecycle.html +14860 -0
  54. package/template/devflow/docs/diagrams/nexus-devflow-lifecycle.json +88 -0
  55. package/template/devflow/docs/examples/living-spec/adr.example.md +43 -0
  56. package/template/devflow/docs/examples/living-spec/current-feature.example.md +113 -0
  57. package/template/devflow/docs/examples/living-spec/discovery.example.md +45 -0
  58. package/template/devflow/docs/examples/living-spec/ideas.example.md +22 -0
  59. package/template/devflow/docs/governance-rules.md +39 -0
  60. package/template/devflow/docs/manual-review-workflow-spec.md +116 -0
  61. package/template/devflow/docs/markdown-metadata-contract.md +140 -0
  62. package/template/devflow/docs/quickstart.md +119 -0
  63. package/template/devflow/docs/release-process.md +52 -0
  64. package/template/devflow/docs/skill-selection-policy.md +64 -0
  65. package/template/devflow/docs/workflow-surface-map.md +85 -0
@@ -1,4 +1,3 @@
1
- import { DASHBOARD_PAGE_HTML } from "./dashboard-page.js";
2
1
  const WEBVIEW_STUDIO_CSS = `
3
2
  * { box-sizing: border-box; margin: 0; padding: 0; }
4
3
  body {
@@ -380,6 +379,667 @@ const WEBVIEW_STUDIO_CSS = `
380
379
  gap: 8px;
381
380
  }
382
381
  `;
382
+ const DASHBOARD_BASE_HTML = `<!doctype html>
383
+ <html lang="th">
384
+ <head>
385
+ <meta charset="utf-8">
386
+ <meta name="viewport" content="width=device-width, initial-scale=1">
387
+ <title>Nexus-DevFlow Enterprise Dashboard</title>
388
+ <link rel="preconnect" href="https://fonts.googleapis.com">
389
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
390
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
391
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
392
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Noto+Sans+Thai:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
393
+ <style>
394
+ :root{
395
+ --font-display:"Google Sans Thai","Google Sans","Noto Sans Thai","Space Grotesk",sans-serif;
396
+ --font-sans:"Google Sans Thai","Google Sans","Noto Sans Thai",sans-serif;
397
+ --font-mono:"IBM Plex Mono",monospace;
398
+ --bg:#0a2540;--bg-deep:#061a2e;--panel:rgba(255,255,255,.04);--panel-strong:#0c2b49;
399
+ --ink:#edf6ff;--soft:#b7d3ef;--muted:#7598ba;--line:rgba(191,224,255,.17);--line-strong:rgba(191,224,255,.36);
400
+ --cyan:#7dd8ff;--mint:#6fe3b4;--gold:#f2c14e;--red:#ff7061;--violet:#b8a4ff;
401
+ --mint-soft:rgba(111,227,180,.14);--gold-soft:rgba(242,193,78,.14);--red-soft:rgba(255,112,97,.14);--violet-soft:rgba(184,164,255,.14);
402
+ }
403
+ *{box-sizing:border-box}
404
+ html{color-scheme:dark}
405
+ body{margin:0;min-height:100vh;background-color:var(--bg);color:var(--ink);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;background-image:linear-gradient(rgba(125,216,255,.065) 1px,transparent 1px),linear-gradient(90deg,rgba(125,216,255,.065) 1px,transparent 1px),radial-gradient(circle at 8% -5%,rgba(125,216,255,.16),transparent 42rem);background-size:34px 34px,34px 34px,auto}
406
+ button,input{font:inherit}
407
+ code{font-family:var(--font-mono);color:var(--cyan)}
408
+ .sheet{width:min(1180px,calc(100% - 40px));margin:0 auto;padding:34px 0 64px}
409
+ .titleblock{display:flex;align-items:flex-end;justify-content:space-between;gap:26px;padding-bottom:20px;border-bottom:1px solid var(--line-strong);flex-wrap:wrap}
410
+ .eyebrow,.label{font:600 10px/1 var(--font-mono);letter-spacing:.13em;text-transform:uppercase;color:var(--cyan)}
411
+ .eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:12px}.eyebrow:before{content:"";width:20px;height:1px;background:var(--cyan)}
412
+ h1{margin:0 0 6px;font:700 clamp(32px,4vw,48px)/1.05 var(--font-display);letter-spacing:-.03em}
413
+ .path,.muted{color:var(--muted);font-size:12px;line-height:1.65}.path{font-family:var(--font-mono)}
414
+ .badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.badge,.pill{padding:4px 9px;border:1px solid var(--line-strong);border-radius:3px;background:rgba(255,255,255,.025);color:var(--muted);font:600 10px/1.5 var(--font-mono);text-transform:uppercase}
415
+ .ok,.healthy,.current,.ready,.idle{border-color:rgba(111,227,180,.42)!important;color:var(--mint)!important;background:var(--mint-soft)!important}
416
+ .warning,.available,.offline,.blocked,.fail{border-color:rgba(255,112,97,.42)!important;color:var(--red)!important;background:var(--red-soft)!important}
417
+ .active,.deep,.fast,.configured{border-color:rgba(184,164,255,.42)!important;color:var(--violet)!important;background:var(--violet-soft)!important}
418
+ .meta{display:grid;grid-template-columns:auto auto;gap:4px 20px;margin:0;font:11px/1.55 var(--font-mono)}.meta dt{color:var(--muted);text-transform:uppercase}.meta dd{margin:0;color:var(--soft);text-align:right}
419
+ .live{display:flex;align-items:center;gap:8px;margin:14px 0 24px;font:11px var(--font-mono);color:var(--muted)}.live-dot{width:7px;height:7px;border-radius:50%;background:var(--gold)}.live-dot.on{background:var(--mint);box-shadow:0 0 0 4px rgba(111,227,180,.12)}
420
+ .panel,.card{position:relative;min-width:0;border:1px solid var(--line);border-radius:4px;background:var(--panel)}
421
+ #dual-track{overflow:hidden}
422
+ .stepper{justify-content:center}.step{display:flex;flex-direction:column;align-items:center;text-align:center}.step-icon{display:grid;place-items:center;width:30px;height:30px;margin:10px auto 8px;border:1px solid currentColor;border-radius:8px;background:rgba(255,255,255,.035)}.icon-glyph{display:block;font-size:17px;line-height:1}.step.flow-0,.step.flow-4{color:var(--cyan)}.step.flow-1,.step.flow-5{color:var(--violet)}.step.flow-2,.step.flow-6{color:var(--gold)}.step.flow-3,.step.flow-7{color:var(--mint)}.step .step-name{color:currentColor}.step .step-cmd{color:var(--muted)}
423
+ .panel{padding:24px 26px;margin-bottom:20px}.panel-head,.card-head{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}.panel-head{margin-bottom:20px}.card-head{margin-bottom:17px}
424
+ .track-tabs{display:flex;gap:7px;flex-wrap:wrap}.tab{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border:1px solid var(--line-strong);border-radius:99px;background:transparent;color:var(--muted);font:600 10px var(--font-mono);cursor:pointer}.tab:hover,.tab:focus-visible{border-color:var(--cyan);color:var(--cyan);outline:none}.tab.on{background:var(--cyan);border-color:var(--cyan);color:var(--bg-deep)}.tab-active-dot{display:none;padding:2px 6px;border-radius:99px;font:700 8px/1 var(--font-mono);letter-spacing:.05em;text-transform:uppercase;background:var(--mint);color:var(--bg-deep)}.tab.is-active-track .tab-active-dot{display:inline-flex;align-items:center;gap:3px;box-shadow:0 0 0 2px rgba(111,227,180,.25)}.tab.on.is-active-track .tab-active-dot{background:var(--bg-deep);color:var(--mint)}
425
+ .track-view{display:none;min-width:0}.track-view.on{display:block}.stepper{display:flex;width:100%;max-width:100%;overflow-x:auto;padding:4px 0 7px}.step{flex:1 1 0;min-width:104px;position:relative;padding-right:12px}.step-line{position:absolute;top:10px;left:21px;right:0;height:1px;background:var(--line-strong)}.step:last-child .step-line{display:none}.step.done .step-line{background:var(--mint)}
426
+ .step-dot{position:relative;z-index:1;width:21px;height:21px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--line-strong);background:var(--bg-deep);color:var(--muted);font:700 8px var(--font-mono)}.step.done .step-dot{border-color:var(--mint);color:var(--mint)}.step.active .step-dot{border-color:var(--gold);color:var(--gold);box-shadow:0 0 0 4px rgba(242,193,78,.16)}
427
+ .step-name{margin-top:9px;color:var(--soft);font:600 10px/1.35 var(--font-mono)}.step.active .step-name{color:var(--gold)}.step.done .step-name{color:var(--mint)}.step-cmd{margin-top:3px;color:var(--muted);font:9px/1.4 var(--font-mono)}.track-note{margin-top:12px;padding-top:12px;border-top:1px dashed var(--line);color:var(--muted);font:11px/1.65 var(--font-mono)}
428
+ .next-action{background:var(--bg-deep);border-color:var(--line-strong)}.next-head{display:flex;justify-content:space-between;gap:12px;align-items:center}.command{margin:8px 0 6px;color:var(--gold);font:700 clamp(22px,3vw,30px)/1.3 var(--font-mono)}.copy{padding:7px 12px;border:1px solid var(--cyan);border-radius:3px;background:transparent;color:var(--cyan);font:600 10px var(--font-mono);cursor:pointer}.copy:hover,.copy:focus-visible{background:var(--cyan);color:var(--bg-deep);outline:none}
429
+ .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:1px;margin:0 0 20px;border:1px solid var(--line);border-radius:4px;overflow:hidden;background:var(--line)}.stat{padding:16px 18px;background:var(--bg-deep)}.stat-value{font:700 25px/1 var(--font-display)}.stat-label{margin-top:6px;color:var(--muted);font:10px var(--font-mono);text-transform:uppercase;letter-spacing:.08em}
430
+ .grid{display:grid;grid-template-columns:repeat(12,1fr);gap:20px;margin-bottom:20px}.card{grid-column:span 4;min-width:0;padding:clamp(20px,2.4vw,28px)}.card.wide{grid-column:span 8}.card.full{grid-column:1/-1}.value{font:700 18px/1.4 var(--font-display)}
431
+ .facts{display:grid;gap:10px}.fact{display:flex;justify-content:space-between;gap:16px;padding-bottom:10px;border-bottom:1px dashed var(--line)}.fact:last-child{padding-bottom:0;border:0}.fact span:first-child{color:var(--muted);font-size:11px}.fact span:last-child{color:var(--soft);font:11px var(--font-mono);text-align:right;overflow-wrap:anywhere}
432
+ .progress{height:6px;margin:14px 0 9px;border-radius:99px;background:rgba(255,255,255,.07);overflow:hidden}.progress span{display:block;height:100%;width:0;background:var(--gold);transition:width .35s ease}
433
+ ul{list-style:none;margin:0;padding:0}li{padding:10px 0;border-bottom:1px dashed var(--line);color:var(--soft);font-size:12px;line-height:1.55}li:last-child{border:0}.empty{color:var(--muted)}
434
+ .severity{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.sev{padding:9px 6px;border:1px solid var(--line);border-radius:3px;text-align:center}.sev b{display:block;font:700 17px var(--font-display)}.sev small{color:var(--muted);font:9px var(--font-mono)}
435
+ .commands{display:flex;flex-wrap:wrap;gap:10px}.cmd{position:relative;padding:8px 13px;border:1px solid var(--line-strong);border-radius:3px;background:rgba(255,255,255,.025);color:var(--soft);font:11px var(--font-mono);cursor:pointer}.cmd:hover,.cmd:focus-visible,.cmd:focus{border-color:var(--cyan);color:var(--cyan);outline:2px solid rgba(125,216,255,.2);outline-offset:2px}.cmd:after{content:attr(data-tip);position:absolute;z-index:30;left:50%;bottom:calc(100% + 10px);width:max-content;max-width:min(300px,80vw);padding:9px 11px;border:1px solid var(--line-strong);border-radius:3px;background:#031421;color:var(--soft);font:10px/1.5 var(--font-mono);white-space:normal;text-align:left;opacity:0;visibility:hidden;transform:translate(-50%,5px);box-shadow:0 12px 30px rgba(0,0,0,.4);transition:.15s;pointer-events:none}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{opacity:1;visibility:visible;transform:translate(-50%,0)}
436
+ .adapters,.roster-grid,.mcp-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.adapter,.roster-card,.mcp-card{padding:15px;border:1px solid var(--line);border-radius:4px;background:rgba(255,255,255,.02)}.adapter strong,.roster-card strong,.mcp-card strong{display:block;margin-bottom:6px;text-transform:capitalize}.adapter-path,.roster-desc,.mcp-desc{color:var(--muted);font:10px var(--font-mono);line-height:1.4}.adapter-state{margin-top:9px;font:10px var(--font-mono)}
437
+ .mcp-grid{grid-template-columns:repeat(3,1fr)}
438
+ .search-row{display:flex;gap:10px;margin-bottom:16px}.search-input{flex:1;padding:10px 14px;background:var(--bg-deep);border:1px solid var(--line-strong);border-radius:4px;color:var(--ink);font-family:var(--font-mono);font-size:12px}.search-btn{padding:10px 18px;border:1px solid var(--cyan);border-radius:4px;background:var(--cyan);color:var(--bg-deep);font-weight:700;cursor:pointer}
439
+ .blast-result{padding:16px;border:1px solid var(--line-strong);border-radius:4px;background:var(--bg-deep);margin-top:12px;display:none}
440
+ .section-note{margin:-2px 0 14px;color:var(--muted);font:10px/1.5 var(--font-mono)}.record-list{display:grid;gap:8px}.record-item{display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:13px;align-items:center;padding:13px 15px;border:1px solid var(--line);border-left:3px solid var(--record-color,var(--cyan));border-radius:3px;background:rgba(255,255,255,.02)}.record-icon{display:grid;place-items:center;width:31px;height:31px;border-radius:8px;background:color-mix(in srgb,var(--record-color,var(--cyan)) 14%,transparent)}.record-body{min-width:0}.record-meta{color:var(--record-color,var(--cyan));font:700 10px var(--font-mono);letter-spacing:.06em;text-transform:uppercase}.record-title{margin-top:3px;color:var(--ink);font-weight:650}.record-desc{margin-top:3px;color:var(--soft);font-size:12px}.record-desc:empty{display:none}.record-title strong,.record-desc strong{color:var(--record-color,var(--cyan));font-weight:750}.record-title em,.record-desc em{color:var(--gold)}.record-title code,.record-desc code{padding:1px 4px;border:1px solid var(--line);border-radius:3px;color:var(--cyan);font:10px var(--font-mono)}.record-title a,.record-desc a{color:var(--cyan);text-decoration:underline}.record-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.record-action{border:1px solid var(--record-color,var(--cyan));border-radius:3px;padding:8px 11px;background:transparent;color:var(--record-color,var(--cyan));font:700 10px var(--font-mono);cursor:pointer}.record-action:hover,.record-action:focus-visible{background:var(--record-color,var(--cyan));color:var(--bg)}.record-badge{align-self:start;border:1px solid rgba(111,227,180,.36);border-radius:99px;padding:4px 8px;background:var(--mint-soft);color:var(--mint);font:700 9px var(--font-mono);letter-spacing:.04em;text-transform:uppercase}.record-item.tone-cyan{--record-color:var(--cyan)}.record-item.tone-mint{--record-color:var(--mint)}.record-item.tone-gold{--record-color:var(--gold)}.record-item.tone-red{--record-color:var(--red)}.record-item.tone-violet{--record-color:var(--violet)}.stepper{gap:0;justify-content:center}.step{padding-right:0;overflow:visible}.step-line{left:calc(50% + 10px);right:calc(-50% + 10px);width:auto}#sev-p0{color:var(--red)}#sev-p1{color:var(--gold)}#sev-p2{color:var(--violet)}#sev-p3{color:var(--cyan)}#stat-runs.tone{color:var(--mint)}#stat-findings.tone-ok{color:var(--mint)}#stat-findings.tone-alert{color:var(--red)}#stat-ideas.tone-ok{color:var(--mint)}#stat-ideas.tone-pending{color:var(--gold)}#stat-adapters.tone-ok{color:var(--mint)}#stat-adapters.tone-partial{color:var(--gold)}#stat-state.tone-idle{color:var(--cyan)}#stat-state.tone-active{color:var(--violet)}#stat-state.tone-blocked{color:var(--red)}
441
+ .multitask-panel{border-color:rgba(56,189,248,.35)}.workspace-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px;margin-top:10px}.workspace-card{background:rgba(255,255,255,.025);border:1px solid var(--line);border-radius:6px;padding:16px;display:flex;flex-direction:column;gap:8px;transition:all .2s ease}.workspace-card:hover{border-color:var(--cyan);background:rgba(255,255,255,.04)}.ws-header{display:flex;align-items:center;justify-content:space-between;gap:8px}.ws-id{font-family:var(--font-mono);font-weight:700;font-size:11px;color:var(--cyan);background:var(--cyan-soft);padding:2px 8px;border-radius:3px}.ws-stage{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;padding:2px 6px;border-radius:3px;background:var(--gold-soft);color:var(--gold);border:1px solid rgba(242,193,78,.3)}.ws-stage.stage-check{background:var(--mint-soft);color:var(--mint);border-color:rgba(111,227,180,.3)}.ws-title{font-weight:650;font-size:13px;color:var(--ink);line-height:1.4}.ws-progress-bar{height:6px;background:rgba(0,0,0,.3);border-radius:99px;overflow:hidden;margin:4px 0}.ws-progress-fill{height:100%;background:linear-gradient(135deg,var(--cyan),var(--violet));transition:width .3s}.ws-meta{display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--muted)}.ws-actions{display:flex;align-items:center;gap:6px;padding-top:8px;border-top:1px dashed var(--line);flex-wrap:wrap}.btn-ws{padding:4px 9px;border-radius:3px;font-family:var(--font-mono);font-size:10px;font-weight:700;background:rgba(255,255,255,.04);border:1px solid var(--line);color:var(--soft);cursor:pointer;transition:all .15s}.btn-ws:hover{background:var(--cyan-soft);border-color:var(--cyan);color:var(--cyan)}
442
+ .archify-visualizer-panel{border-color:rgba(125,216,255,.3);background:var(--bg-deep);overflow:hidden;margin-bottom:20px}
443
+ .viz-controls{display:flex;gap:8px}
444
+ .viz-tab{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--line-strong);border-radius:99px;background:transparent;color:var(--muted);font:600 10px var(--font-mono);cursor:pointer}
445
+ .viz-tab:hover,.viz-tab:focus-visible{border-color:var(--cyan);color:var(--cyan)}
446
+ .viz-tab.on{background:var(--cyan);border-color:var(--cyan);color:var(--bg-deep)}
447
+ .visualizer-canvas-container{width:100%;overflow-x:auto;padding:12px 0;display:flex;justify-content:center}
448
+ .visualizer-canvas-container svg{max-width:100%;height:auto}
449
+ @keyframes archify-trace-motion{0%{stroke-dashoffset:48}100%{stroke-dashoffset:0}}
450
+ @keyframes archify-pulse{0%{transform:scale(1);opacity:.8}50%{transform:scale(1.15);opacity:.2}100%{transform:scale(1);opacity:.8}}
451
+ .viz-active-trace{stroke-dasharray:6 6;animation:archify-trace-motion 1.2s linear infinite}
452
+ .viz-pulse-circle{transform-origin:center;animation:archify-pulse 2s ease-in-out infinite}
453
+ .skills-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin-top:10px}
454
+ .skill-card{background:rgba(255,255,255,.025);border:1px solid var(--line);border-radius:6px;padding:14px 16px;display:flex;flex-direction:column;justify-content:space-between;gap:10px;transition:all .2s ease}
455
+ .skill-card:hover{border-color:var(--line-strong);background:rgba(255,255,255,.04)}
456
+ .skill-card.skill-installed{border-left:3px solid var(--mint)}
457
+ .skill-card.skill-missing{border-left:3px solid var(--muted)}
458
+ .skill-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
459
+ .skill-title-block{display:flex;flex-direction:column;gap:2px}
460
+ .skill-title{font-size:13px;color:var(--ink);font-weight:650}
461
+ .skill-name code{font-size:10px}
462
+ .skill-cat{font-family:var(--font-mono);font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:.05em}
463
+ .skill-cat.vendor{background:var(--violet-soft);color:var(--violet);border:1px solid rgba(184,164,255,.3)}
464
+ .skill-cat.skill{background:var(--cyan-soft);color:var(--cyan);border:1px solid rgba(125,216,255,.3)}
465
+ .skill-desc{font-size:11px;color:var(--muted);line-height:1.45;margin:0}
466
+ .skill-footer{display:flex;justify-content:flex-end;align-items:center;padding-top:8px;border-top:1px dashed var(--line)}
467
+ .badge-installed{font-family:var(--font-mono);font-size:10px;font-weight:700;color:var(--mint);background:var(--mint-soft);border:1px solid rgba(111,227,180,.3);padding:3px 8px;border-radius:99px;display:inline-flex;align-items:center;gap:4px}
468
+ .btn-install-cmd{font-family:var(--font-mono);font-size:10px;font-weight:600;padding:4px 10px;border-radius:4px;background:var(--cyan-soft);color:var(--cyan);border:1px solid rgba(125,216,255,.3);cursor:pointer;transition:all .15s ease}
469
+ .btn-install-cmd:hover{background:var(--cyan);color:var(--bg-deep)}
470
+ .doctor-compact-summary{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:rgba(255,255,255,.025);border:1px solid var(--line);border-radius:6px;gap:12px;flex-wrap:wrap}
471
+ .btn-doctor-toggle{padding:4px 10px;border-radius:4px;font-family:var(--font-mono);font-size:10px;font-weight:700;background:var(--panel);border:1px solid var(--line);color:var(--soft);cursor:pointer;transition:all .15s}
472
+ .btn-doctor-toggle:hover{border-color:var(--cyan);color:var(--cyan)}
473
+ footer{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:26px;padding-top:17px;border-top:1px solid var(--line);color:var(--muted);font:10px var(--font-mono)}
474
+ @media(max-width:920px){.grid .card,.grid .card.wide{grid-column:1/-1}.stats{grid-template-columns:repeat(2,1fr)}.adapters,.roster-grid,.mcp-grid{grid-template-columns:repeat(2,1fr)}.titleblock{align-items:flex-start;flex-direction:column}.meta dd{text-align:left}.stepper{gap:0}.cmd:after{position:fixed;left:12px;right:12px;bottom:16px;width:auto;max-width:none;transform:translateY(5px)}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{transform:none}}
475
+ @media(max-width:560px){.sheet{width:calc(100% - 24px);padding-top:22px}.panel,.card{padding:18px}.grid{gap:14px;margin-bottom:14px}.stats{margin-bottom:14px}.adapters,.roster-grid,.mcp-grid{grid-template-columns:1fr}.cmd:after{position:fixed;left:12px;right:12px;bottom:16px;width:auto;max-width:none;transform:translateY(5px)}.cmd:hover:after,.cmd:focus-visible:after,.cmd:focus:after{transform:none}.meta{grid-template-columns:1fr}.track-tabs{width:100%}.tab{flex:1}.fact{align-items:flex-start}.record-item{grid-template-columns:34px minmax(0,1fr)}.record-action{grid-column:2;justify-self:start}}
476
+ @media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.001ms!important;transition-duration:.001ms!important}}
477
+ </style>
478
+ </head>
479
+ <body>
480
+ <main class="sheet">
481
+ <header class="titleblock">
482
+ <div>
483
+ <div class="eyebrow">Nexus-DevFlow · Enterprise Workspace Status</div>
484
+ <h1 id="project-name">Nexus-DevFlow</h1>
485
+ <div class="path" id="project-path">Connecting...</div>
486
+ <div class="badges">
487
+ <span class="badge" id="version-badge">v2.5.0</span>
488
+ <span class="badge" id="health-badge">health</span>
489
+ <span class="badge" id="gate-badge">Gate: checking</span>
490
+ <span class="badge" id="drift-badge">Drift: checking</span>
491
+ <span class="badge" id="track-badge">track</span>
492
+ </div>
493
+ </div>
494
+ <dl class="meta">
495
+ <dt>Package</dt><dd id="package-name">@jakkrichm/create-nexus-devflow</dd>
496
+ <dt>Adapters</dt><dd id="adapter-count">-</dd>
497
+ <dt>Branch</dt><dd id="header-branch">-</dd>
498
+ <dt>Snapshot</dt><dd id="generated-at">-</dd>
499
+ </dl>
500
+ </header>
501
+ <div class="live"><span class="live-dot" id="live-dot"></span><span id="live-label">Connecting to local dashboard...</span></div>
502
+
503
+ <!-- Dynamic Archify Workflow Visualizer -->
504
+ <section class="panel archify-visualizer-panel" id="workflow-visualizer-panel">
505
+ <div class="panel-head">
506
+ <div>
507
+ <span class="label">🎨 Archify Dynamic Workflow Visualizer</span>
508
+ <div class="muted" style="margin-top:3px;font-size:11px;">Real-time SVG Trace-Motion Architecture &amp; Dependency DAG</div>
509
+ </div>
510
+ <div class="viz-controls">
511
+ <button class="viz-tab on" id="viz-btn-macro" type="button">⚡ Macro: Lifecycle Pulse</button>
512
+ <button class="viz-tab" id="viz-btn-micro" type="button">🎫 Micro: Tickets DAG</button>
513
+ <button class="viz-tab" id="viz-btn-graph" type="button">🗺️ Code Graph</button>
514
+ </div>
515
+ </div>
516
+ <div class="visualizer-canvas-container" id="visualizer-container"></div>
517
+ <div class="visualizer-graph-container" id="visualizer-graph-container" style="display:none;padding:16px 20px;">
518
+ <div class="section-note" style="color:var(--muted);font-size:12px;margin-bottom:10px;">Semantic dependency graph &amp; instant Blast Radius analyzer</div>
519
+ <div class="search-row" style="display:flex;gap:10px;">
520
+ <input class="search-input" id="graph-file-input" style="flex:1;background:rgba(255,255,255,0.04);border:1px solid rgba(191,224,255,0.15);color:var(--text);padding:8px 12px;border-radius:6px;font-family:var(--font-mono);font-size:12px;" placeholder="Type file path (e.g. packages/create-nexus-devflow/lib/code-graph.ts)..." />
521
+ <button class="search-btn" id="graph-search-btn" type="button" style="background:var(--card);border:1px solid rgba(191,224,255,0.2);color:var(--text);padding:8px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;">Analyze Impact</button>
522
+ </div>
523
+ <div class="blast-result" id="blast-result" style="display:none;margin-top:14px;padding:12px;background:rgba(0,0,0,0.2);border-radius:6px;font-size:12px;font-family:var(--font-mono);"></div>
524
+ </div>
525
+ </section>
526
+
527
+ <section class="panel next-action" id="next-panel">
528
+ <div class="next-head"><span class="label">Next Action</span><button class="copy" id="next-copy" type="button">Copy command</button></div>
529
+ <div class="command" id="next-command" aria-live="polite">Loading...</div>
530
+ <div class="muted" id="next-reason"></div>
531
+ </section>
532
+
533
+ <section class="stats" aria-label="Workspace summary">
534
+ <div class="stat"><div class="stat-value" id="stat-runs">-</div><div class="stat-label">Runs released</div></div>
535
+ <div class="stat"><div class="stat-value" id="stat-findings">-</div><div class="stat-label">Active findings</div></div>
536
+ <div class="stat"><div class="stat-value" id="stat-ideas">-</div><div class="stat-label">Pending ideas</div></div>
537
+ <div class="stat"><div class="stat-value" id="stat-mcp">-</div><div class="stat-label">MCP Tools Ready</div></div>
538
+ <div class="stat"><div class="stat-value" id="stat-state">-</div><div class="stat-label">Current state</div></div>
539
+ </section>
540
+
541
+ <!-- Multi-Task Living Spec Workspaces Grid -->
542
+ <section class="card full panel multitask-panel" id="active-workspaces-panel">
543
+ <div class="panel-head">
544
+ <div>
545
+ <span class="label">Active Living Specs (Pure Task-Isolated Workspaces)</span>
546
+ <div class="muted" id="active-workspaces-summary" style="margin-top:3px;font-size:11px;">Scanned from devflow/context/{xxx-slug}/</div>
547
+ </div>
548
+ <span class="pill active" id="active-workspaces-pill">0 Active</span>
549
+ </div>
550
+ <div class="workspace-grid" id="active-workspaces-list"></div>
551
+ </section>
552
+
553
+ <!-- Recommended Skills & Vendor Ecosystem Hub -->
554
+ <section class="card full panel" id="recommended-skills-panel">
555
+ <div class="card-head">
556
+ <div>
557
+ <span class="label">🌟 Recommended Skills & Vendor Ecosystem</span>
558
+ <div class="muted" style="margin-top:3px;font-size:11px;">Curated companion packages & JIT capabilities for modern AI development</div>
559
+ </div>
560
+ <span class="pill" id="skills-count-pill">-</span>
561
+ </div>
562
+ <div class="skills-grid" id="recommended-skills-list"></div>
563
+ </section>
564
+
565
+ <section class="grid">
566
+ <article class="card"><div class="card-head"><span class="label">Nexus-DevFlow</span><span class="pill" id="update-pill">checking</span></div><div class="facts"><div class="fact"><span>Installed / latest</span><span id="installed-latest">-</span></div><div class="fact"><span>Health</span><span id="system-health">-</span></div><div class="fact"><span>Architecture</span><span>3-Pillars & Task-Isolated Spec</span></div></div></article>
567
+ <article class="card wide"><div class="card-head"><span class="label">Current Work</span><span class="pill" id="work-pill">loading</span></div><div class="value" id="work-title">Loading...</div><div class="progress"><span id="work-progress"></span></div><div class="muted" id="work-meta"></div></article>
568
+ <article class="card"><div class="card-head"><span class="label">Git & Drift</span><span class="pill" id="git-pill">loading</span></div><div class="facts"><div class="fact"><span>Branch</span><span id="git-branch">-</span></div><div class="fact"><span>Working tree</span><span id="git-changed">-</span></div><div class="fact"><span>Drift Status</span><span id="git-drift">-</span></div><div class="fact"><span>Last commit</span><span id="git-commit">-</span></div></div></article>
569
+ <article class="card"><div class="card-head"><span class="label">Findings Ledger</span><span class="value" id="findings-count">-</span></div><div class="severity"><div class="sev"><b id="sev-p0">0</b><small>P0</small></div><div class="sev"><b id="sev-p1">0</b><small>P1</small></div><div class="sev"><b id="sev-p2">0</b><small>P2</small></div><div class="sev"><b id="sev-p3">0</b><small>P3</small></div></div><ul id="findings-list"></ul></article>
570
+ <article class="card"><div class="card-head"><span class="label">Completion & Gate</span><span class="pill" id="completion-pill">-</span></div><ul id="completion-list"></ul></article>
571
+ <article class="card"><div class="card-head"><span class="label">Independent Review</span><span class="pill" id="review-state">none</span></div><div class="facts"><div class="fact"><span>Verdict</span><span id="review-verdict">-</span></div><div class="fact"><span>Freshness</span><span id="review-freshness">-</span></div><div class="fact"><span>Check</span><span id="review-check">-</span></div></div><ul id="review-warnings"></ul></article>
572
+ <article class="card"><div class="card-head"><span class="label">Warnings</span><span class="value" id="warnings-count">-</span></div><ul id="warnings-list"></ul></article>
573
+ </section>
574
+
575
+ <section class="card full panel"><div class="card-head"><span class="label">Model Context Protocol (MCP) Tools Hub</span><span class="value" id="mcp-count">12 Tools</span></div><div class="section-note">Typed JSON-RPC tools exposed to AI Coding Assistants</div><div class="mcp-grid" id="mcp-list"></div></section>
576
+ <section class="card full panel"><div class="card-head"><span class="label">Quick Commands</span><span class="muted">hover or focus for help · click to copy</span></div><div class="commands" id="command-list"></div></section>
577
+ <section class="grid"><article class="card full"><div class="card-head"><span class="label">Doctor · Health Check</span><span class="pill" id="doctor-pill">-</span></div><div class="record-list" id="doctor-list"></div></article></section>
578
+ <section class="card full panel"><div class="card-head"><span class="label">Discoveries</span><span class="value" id="discovery-count">-</span></div><div class="record-list" id="discovery-list"></div></section>
579
+ <section class="card full panel"><div class="card-head"><span class="label">Idea Inbox & Backlog</span><span class="value" id="ideas-count">-</span></div><div class="section-note">From devflow/ideas.md · start a selected item with /feature IDEA-xxx</div><div class="record-list" id="ideas-list"></div></section>
580
+ <section class="card full panel"><div class="card-head"><span class="label">Completed Work Archive</span><span class="value" id="history-count">-</span></div><div class="section-note">Categorized delivery history · features, fixes and rollbacks</div><div class="record-list" id="history-list"></div></section>
581
+ <footer><span>Nexus-DevFlow 2.5.0 Enterprise Dashboard · refreshes every 2 seconds</span><span id="footer-revision">snapshot pending</span></footer>
582
+ </main>
583
+ <script>
584
+ const byId = (id) => document.getElementById(id);
585
+ const escapeHtml = (str) => String(str == null ? '' : str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
586
+ const text = (id, value) => { const node = byId(id); if (node) node.textContent = value == null ? '-' : String(value); };
587
+ const pill = (id, state, label) => { const node = byId(id); if (!node) return; node.className = 'pill ' + (state || 'configured'); node.textContent = label || state || '-'; };
588
+ const list = (id, values, empty) => { const node = byId(id); if (!node) return; node.replaceChildren(); const rows = values.length ? values : [empty]; rows.forEach((value) => { const li = document.createElement('li'); li.textContent = value; if (!values.length) li.className = 'empty'; node.append(li); }); };
589
+ const titleCase = (value) => String(value || '').replaceAll('_',' ').replace(/\b\w/g, (c) => c.toUpperCase());
590
+ const FLOW_ICON_NAMES = ['target','code','shield','package'];
591
+ const EMOJI_ICONS = { compass:'🔎',discovery:'🔎',target:'🎯',document:'📄',list:'🧩',code:'⚙️',shield:'🧪',report:'📊',package:'📦',bulb:'💡',check:'✅',wrench:'🛠️',rollback:'↩️',search:'🔍',alert:'⚠️',grill:'🔥',brainstorm:'💭',idea:'💡' };
592
+ function iconImage(name) { const icon = document.createElement('span'); icon.className = 'icon-glyph'; icon.setAttribute('aria-hidden','true'); icon.textContent = EMOJI_ICONS[name] || '📄'; return icon; }
593
+ function decoratePipeline(id) { byId(id)?.querySelectorAll('.step').forEach((step,index) => { step.classList.add('flow-' + index); const icon = document.createElement('span'); icon.className = 'step-icon'; icon.setAttribute('aria-hidden','true'); const cmd = step.querySelector('.step-cmd')?.textContent?.replace('/','').split(' ')[0] || ''; const iconKey = id.includes('preflight') ? (cmd || ['idea','grill','brainstorm','discovery'][index] || 'search') : (FLOW_ICON_NAMES[index] || 'document'); icon.append(iconImage(iconKey)); const name = step.querySelector('.step-name'); step.insertBefore(icon,name || null); }); }
594
+ function richText(root,value) { const source = String(value || ''); let cursor = 0; const plain = (end) => { if (end > cursor) root.append(document.createTextNode(source.slice(cursor,end))); cursor = end; }; while (cursor < source.length) { if (source.startsWith('**',cursor)) { const end = source.indexOf('**',cursor + 2); if (end > cursor + 2) { const node = document.createElement('strong'); node.textContent = source.slice(cursor + 2,end); root.append(node); cursor = end + 2; continue; } } if (source[cursor] === '*') { const end = source.indexOf('*',cursor + 1); if (end > cursor + 1) { const node = document.createElement('em'); node.textContent = source.slice(cursor + 1,end); root.append(node); cursor = end + 1; continue; } } if (source.charCodeAt(cursor) === 96) { const end = source.indexOf(String.fromCharCode(96),cursor + 1); if (end > cursor + 1) { const node = document.createElement('code'); node.textContent = source.slice(cursor + 1,end); root.append(node); cursor = end + 1; continue; } } const next = [source.indexOf('**',cursor + 1),source.indexOf('*',cursor + 1),source.indexOf(String.fromCharCode(96),cursor + 1)].filter((index) => index >= 0).sort((a,b) => a-b)[0] ?? source.length; plain(next > cursor ? next : cursor + 1); } }
595
+ function recordCard(options) { const card = document.createElement('article'); card.className = 'record-item tone-' + (options.tone || 'cyan'); const icon = document.createElement('span'); icon.className = 'record-icon'; icon.append(iconImage(options.icon || 'document')); const body = document.createElement('div'); body.className = 'record-body'; const meta = document.createElement('div'); meta.className = 'record-meta'; richText(meta,options.meta); const title = document.createElement('div'); title.className = 'record-title'; richText(title,options.title); const desc = document.createElement('div'); desc.className = 'record-desc'; richText(desc,options.description); body.append(meta,title,desc); card.append(icon,body); if (options.badge) { const badge = document.createElement('span'); badge.className = 'record-badge'; badge.textContent = options.badge; card.append(badge); } return card; }
596
+ function renderRecords(id,items,empty,mapper) { const root = byId(id); root.replaceChildren(); if (!items?.length) { const node = document.createElement('div'); node.className = 'empty'; node.textContent = empty; root.append(node); return; } items.forEach((item,index) => root.append(recordCard(mapper(item,index)))); }
597
+ function renderIdeas(items) { renderRecords('ideas-list',items,'No pending ideas',(item) => ({ icon:'bulb',tone:'gold',meta:item.id,title:item.title,description:(item.description || '⚡') + ' · ' + (item.feasibility || 'Not scored') })); }
598
+ function renderHistory(items) { const map = { feature:{icon:'package',tone:'cyan'},fix:{icon:'wrench',tone:'gold'},rollback:{icon:'rollback',tone:'red'} }; renderRecords('history-list',(items || []).slice(0,10),'No completed work in history archive',(item) => { const type = String(item.type || 'other').toLowerCase(); const style = map[type] || {icon:'report',tone:'violet'}; return { ...style,meta:type + ' ' + (item.buildPlanItem || ''),title:item.title,description:'',badge:'Released' }; }); }
599
+ function renderDoctor(items) {
600
+ const root = byId('doctor-list');
601
+ if (!root) return;
602
+ root.replaceChildren();
603
+ if (!items?.length) {
604
+ const node = document.createElement('div');
605
+ node.className = 'empty';
606
+ node.textContent = 'No doctor checks available';
607
+ root.append(node);
608
+ return;
609
+ }
610
+ const hasIssues = items.some(i => i.status === 'fail' || i.status === 'warn');
611
+ if (!hasIssues) {
612
+ const summary = document.createElement('div');
613
+ summary.className = 'doctor-compact-summary';
614
+ summary.innerHTML = '<span style="color:var(--mint);font-size:12px;font-weight:700;">✔ All ' + items.length + ' System Integrity Checks Passed (Environment, Config, Adapters, Living Specs)</span>' +
615
+ '<button class="btn-doctor-toggle" type="button" id="doctor-toggle-btn">Show Details (' + items.length + ')</button>';
616
+ const detailsContainer = document.createElement('div');
617
+ detailsContainer.className = 'record-list';
618
+ detailsContainer.style.display = 'none';
619
+ detailsContainer.style.marginTop = '10px';
620
+ items.forEach((item) => {
621
+ detailsContainer.append(recordCard({ icon: 'check', tone: 'mint', meta: item.status, title: item.name, description: item.message }));
622
+ });
623
+ const toggleBtn = summary.querySelector('#doctor-toggle-btn');
624
+ if (toggleBtn) {
625
+ toggleBtn.addEventListener('click', () => {
626
+ const isHidden = detailsContainer.style.display === 'none';
627
+ detailsContainer.style.display = isHidden ? 'grid' : 'none';
628
+ toggleBtn.textContent = isHidden ? 'Hide Details' : 'Show Details (' + items.length + ')';
629
+ });
630
+ }
631
+ root.append(summary, detailsContainer);
632
+ } else {
633
+ items.forEach((item) => root.append(recordCard({
634
+ icon: item.status === 'pass' ? 'check' : 'alert',
635
+ tone: item.status === 'pass' ? 'mint' : item.status === 'warn' ? 'gold' : 'red',
636
+ meta: item.status,
637
+ title: item.name,
638
+ description: item.message
639
+ })));
640
+ }
641
+ }
642
+ function renderDiscoveries(items) { renderRecords('discovery-list',items,'No discoveries recorded',(item) => ({ icon:'search',tone:'mint',meta:item.id,title:item.title,description:'',badge:'Decision: ' + (item.decision || 'Pending') })); }
643
+
644
+ function renderPipeline(id, nodes) {
645
+ const root = byId(id); root.replaceChildren();
646
+ (nodes || []).forEach((node, index) => {
647
+ const step = document.createElement('div'); step.className = 'step ' + node.state;
648
+ const line = document.createElement('span'); line.className = 'step-line';
649
+ const dot = document.createElement('span'); dot.className = 'step-dot'; dot.textContent = node.state === 'done' ? '✓' : String(index + 1).padStart(nodes.length > 4 ? 2 : 1, '0');
650
+ const name = document.createElement('div'); name.className = 'step-name'; name.textContent = node.label;
651
+ const command = document.createElement('div'); command.className = 'step-cmd'; command.textContent = node.command;
652
+ step.append(line, dot, name, command); root.append(step);
653
+ });
654
+ }
655
+
656
+ let userHasSwitchedTab = false;
657
+ function selectTrack(track) {
658
+ document.querySelectorAll('.tab').forEach((node) => { const on = node.dataset.track === track; node.classList.toggle('on', on); node.setAttribute('aria-selected', String(on)); });
659
+ document.querySelectorAll('.track-view').forEach((node) => node.classList.toggle('on', node.id === 'view-' + track));
660
+ }
661
+
662
+ function wireCopy(button, getValue) {
663
+ button.addEventListener('click', async () => {
664
+ const original = button.textContent;
665
+ button.textContent = 'Copied!';
666
+ try { await navigator.clipboard.writeText(getValue()); }
667
+ catch { button.textContent = 'Copy unavailable'; }
668
+ setTimeout(() => { button.textContent = original; }, 1100);
669
+ });
670
+ }
671
+
672
+ document.querySelectorAll('.tab').forEach((node) => node.addEventListener('click', () => { userHasSwitchedTab = true; selectTrack(node.dataset.track); }));
673
+ wireCopy(byId('next-copy'), () => byId('next-command').textContent.trim());
674
+
675
+ function renderCommands(commands) {
676
+ const root = byId('command-list'); root.replaceChildren();
677
+ const allCommands = [...(commands || [])];
678
+ ['nexus-devflow check-gate', 'nexus-devflow drift', 'nexus-devflow reconcile --fix', 'nexus-devflow slice --stage implement', 'nexus-devflow swarm', 'nexus-devflow studio'].forEach(c => {
679
+ if (!allCommands.some(item => item.command === c)) {
680
+ allCommands.push({ name: c.replace('nexus-devflow ', ''), command: c, family: 'companion', description: 'Run ' + c });
681
+ }
682
+ });
683
+ allCommands.forEach((item) => {
684
+ const button = document.createElement('button'); button.type = 'button'; button.className = 'cmd'; button.textContent = item.name; button.dataset.tip = item.description; wireCopy(button, () => item.command); root.append(button);
685
+ });
686
+ }
687
+
688
+ let currentVizMode = 'macro';
689
+ let currentSnapshotData = null;
690
+
691
+ function resolveClientStage(data) {
692
+ if (data?.workflow?.currentStage) return data.workflow.currentStage;
693
+ const activeRun = data?.status?.activeRuns?.[0];
694
+ if (activeRun?.stage) return activeRun.stage;
695
+ if (activeRun?.status) return activeRun.status;
696
+ if (data?.status?.currentWork?.status) return data.status.currentWork.status;
697
+ const nextCmd = data?.nextAction?.command || data?.status?.nextAction?.command;
698
+ if (nextCmd) {
699
+ if (nextCmd.includes('complete') || nextCmd.includes('merge')) return 'complete';
700
+ if (nextCmd.includes('check') || nextCmd.includes('verif') || nextCmd.includes('test')) return 'check';
701
+ if (nextCmd.includes('implement') || nextCmd.includes('build')) return 'implement';
702
+ if (nextCmd.includes('feature') || nextCmd.includes('spec')) return 'feature';
703
+ }
704
+ return 'feature';
705
+ }
706
+
707
+ function renderMacroSvg(activeStage) {
708
+ const stageOrder = ['feature', 'implement', 'check', 'complete'];
709
+ function normalize(raw) {
710
+ const s = String(raw || '').toLowerCase().trim();
711
+ if (s.includes('complete') || s.includes('archive') || s.includes('merge') || s.includes('done')) return 'complete';
712
+ if (s.includes('check') || s.includes('verif') || s.includes('qa') || s.includes('test')) return 'check';
713
+ if (s.includes('implement') || s.includes('progress') || s.includes('build') || s.includes('code') || s.includes('dev')) return 'implement';
714
+ return 'feature';
715
+ }
716
+ const activeId = normalize(activeStage);
717
+ const activeIdx = stageOrder.indexOf(activeId);
718
+
719
+ const stages = [
720
+ { id: 'feature', label: '/feature', desc: 'Discover & Spec', icon: '🎯', x: 60 },
721
+ { id: 'implement', label: '/implement', desc: 'TDD Build', icon: '⚙️', x: 270 },
722
+ { id: 'check', label: '/check', desc: 'QA Proof', icon: '🧪', x: 480 },
723
+ { id: 'complete', label: '/complete', desc: 'Archive & Merge', icon: '📦', x: 690 },
724
+ ];
725
+ let connectors = '';
726
+ for (let i = 0; i < stages.length - 1; i++) {
727
+ const from = stages[i]; const to = stages[i + 1];
728
+ const x1 = from.x + 130; const x2 = to.x; const y = 60;
729
+ const isPastOrActive = i < activeIdx;
730
+ const stroke = isPastOrActive ? '#7dd8ff' : 'rgba(191, 224, 255, 0.25)';
731
+ const traceClass = isPastOrActive ? 'viz-active-trace' : '';
732
+ connectors += '<line x1="' + x1 + '" y1="' + y + '" x2="' + x2 + '" y2="' + y + '" stroke="' + stroke + '" stroke-width="' + (isPastOrActive ? 3 : 2) + '" class="' + traceClass + '" />' +
733
+ '<polygon points="' + (x2-6) + ',' + (y-4) + ' ' + x2 + ',' + y + ' ' + (x2-6) + ',' + (y+4) + '" fill="' + stroke + '" />';
734
+ }
735
+ const nodes = stages.map((s, idx) => {
736
+ const isDone = idx < activeIdx;
737
+ const isActive = idx === activeIdx;
738
+ let stroke = 'rgba(191, 224, 255, 0.2)';
739
+ let fill = 'rgba(255, 255, 255, 0.02)';
740
+ let labelFill = '#7598ba';
741
+ let statusBadge = '<text x="' + (s.x + 120) + '" y="42" text-anchor="end" fill="#7598ba" font-family="var(--font-mono)" font-size="9">⏳ Next</text>';
742
+ let glowFilter = '';
743
+ let activeClass = '';
744
+ let pulseRing = '';
745
+
746
+ if (isDone) {
747
+ stroke = '#6fe3b4';
748
+ fill = 'rgba(111, 227, 180, 0.08)';
749
+ labelFill = '#6fe3b4';
750
+ statusBadge = '<text x="' + (s.x + 120) + '" y="42" text-anchor="end" fill="#6fe3b4" font-family="var(--font-mono)" font-size="9" font-weight="700">✔ Done</text>';
751
+ } else if (isActive) {
752
+ stroke = '#7dd8ff';
753
+ fill = 'rgba(125, 216, 255, 0.16)';
754
+ labelFill = '#edf6ff';
755
+ statusBadge = '<text x="' + (s.x + 120) + '" y="42" text-anchor="end" fill="#7dd8ff" font-family="var(--font-mono)" font-size="9" font-weight="700">● Active</text>';
756
+ glowFilter = 'filter="url(#archify-glow)"';
757
+ activeClass = 'is-active';
758
+ pulseRing = '<circle cx="' + (s.x + 24) + '" cy="60" r="18" fill="none" stroke="#7dd8ff" stroke-width="2" class="viz-pulse-circle" opacity="0.8"/>';
759
+ }
760
+
761
+ return '<g class="viz-stage-node ' + activeClass + '">' +
762
+ '<rect x="' + s.x + '" y="25" width="130" height="70" rx="8" fill="' + fill + '" stroke="' + stroke + '" stroke-width="' + (isActive ? 2 : 1) + '" ' + glowFilter + ' />' +
763
+ pulseRing +
764
+ '<text x="' + (s.x + 16) + '" y="65" font-size="18">' + s.icon + '</text>' +
765
+ '<text x="' + (s.x + 44) + '" y="52" fill="' + labelFill + '" font-family="var(--font-mono)" font-size="12" font-weight="700">' + s.label + '</text>' +
766
+ '<text x="' + (s.x + 44) + '" y="72" fill="#7598ba" font-family="var(--font-sans)" font-size="10">' + s.desc + '</text>' +
767
+ statusBadge +
768
+ '</g>';
769
+ }).join('');
770
+ return '<svg viewBox="0 0 880 120" width="100%" height="120" xmlns="http://www.w3.org/2000/svg">' +
771
+ '<defs><filter id="archify-glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="6" result="blur"/><feComposite in="SourceGraphic" in2="blur" operator="over"/></filter></defs>' +
772
+ connectors + nodes + '</svg>';
773
+ }
774
+
775
+ function renderTicketsDagSvg(tickets) {
776
+ if (!tickets || tickets.length === 0) {
777
+ return '<svg viewBox="0 0 880 120" width="100%" height="120" xmlns="http://www.w3.org/2000/svg">' +
778
+ '<rect x="20" y="20" width="840" height="80" rx="8" fill="rgba(255,255,255,0.02)" stroke="rgba(191,224,255,0.15)" stroke-dasharray="4 4" />' +
779
+ '<text x="440" y="66" text-anchor="middle" fill="#7598ba" font-family="var(--font-mono)" font-size="12">No active tracer-bullet tickets found in devflow/context/{xxx-slug}/tickets/.</text>' +
780
+ '</svg>';
781
+ }
782
+ const cardWidth = 170; const cardHeight = 65; const gapX = 50; const startX = 40; const startY = 30;
783
+ const positions = new Map();
784
+ tickets.forEach((t, i) => { positions.set(t.id, { x: startX + i * (cardWidth + gapX), y: startY }); });
785
+ const totalWidth = Math.max(880, startX * 2 + tickets.length * (cardWidth + gapX));
786
+ let connectors = '';
787
+ tickets.forEach((ticket) => {
788
+ const toPos = positions.get(ticket.id); if (!toPos) return;
789
+ (ticket.blockedBy || []).forEach((blockerId) => {
790
+ const fromPos = positions.get(blockerId); if (!fromPos) return;
791
+ const x1 = fromPos.x + cardWidth; const y1 = fromPos.y + cardHeight / 2;
792
+ const x2 = toPos.x; const y2 = toPos.y + cardHeight / 2;
793
+ connectors += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" stroke="#7dd8ff" stroke-width="2" stroke-dasharray="4 4" marker-end="url(#arrow)" />';
794
+ });
795
+ });
796
+ const cards = tickets.map(t => {
797
+ const pos = positions.get(t.id) || { x: 0, y: 0 };
798
+ const statusColor = t.status === 'done' ? '#6fe3b4' : t.status === 'in_progress' ? '#7dd8ff' : '#ff7061';
799
+ const statusBg = t.status === 'done' ? 'rgba(111, 227, 180, 0.12)' : t.status === 'in_progress' ? 'rgba(125, 216, 255, 0.15)' : 'rgba(255, 112, 97, 0.12)';
800
+ const statusLabel = t.status === 'done' ? '✔ Done' : t.status === 'in_progress' ? '▶ Active' : '⏳ Blocked';
801
+ const titleEsc = escapeHtml(t.title.length > 20 ? t.title.slice(0, 19) + '…' : t.title);
802
+ return '<g class="viz-ticket-node ticket-' + t.status + '">' +
803
+ '<rect x="' + pos.x + '" y="' + pos.y + '" width="' + cardWidth + '" height="' + cardHeight + '" rx="6" fill="' + statusBg + '" stroke="' + statusColor + '" stroke-width="1.5" />' +
804
+ '<text x="' + (pos.x + 12) + '" y="' + (pos.y + 22) + '" fill="' + statusColor + '" font-family="var(--font-mono)" font-size="11" font-weight="700">' + escapeHtml(t.id) + '</text>' +
805
+ '<text x="' + (pos.x + cardWidth - 12) + '" y="' + (pos.y + 22) + '" text-anchor="end" fill="' + statusColor + '" font-family="var(--font-mono)" font-size="9" font-weight="600">' + statusLabel + '</text>' +
806
+ '<text x="' + (pos.x + 12) + '" y="' + (pos.y + 44) + '" fill="#edf6ff" font-family="var(--font-sans)" font-size="11" font-weight="500">' + titleEsc + '</text>' +
807
+ '</g>';
808
+ }).join('');
809
+ return '<svg viewBox="0 0 ' + totalWidth + ' 130" width="100%" height="130" xmlns="http://www.w3.org/2000/svg">' +
810
+ '<defs><marker id="arrow" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 1 L 8 5 L 0 9 z" fill="#7dd8ff"/></marker></defs>' +
811
+ connectors + cards + '</svg>';
812
+ }
813
+
814
+ function updateVisualizer(data) {
815
+ const container = byId('visualizer-container');
816
+ if (!container) return;
817
+ if (currentVizMode === 'macro') {
818
+ const stage = resolveClientStage(data);
819
+ container.innerHTML = renderMacroSvg(stage);
820
+ } else if (currentVizMode === 'micro') {
821
+ container.innerHTML = renderTicketsDagSvg(data?.activeTickets || []);
822
+ }
823
+ }
824
+
825
+ byId('viz-btn-macro')?.addEventListener('click', () => {
826
+ currentVizMode = 'macro';
827
+ byId('viz-btn-macro').classList.add('on');
828
+ byId('viz-btn-micro').classList.remove('on');
829
+ byId('viz-btn-graph')?.classList.remove('on');
830
+ byId('visualizer-container').style.display = 'block';
831
+ const g = byId('visualizer-graph-container'); if (g) g.style.display = 'none';
832
+ if (currentSnapshotData) updateVisualizer(currentSnapshotData);
833
+ });
834
+
835
+ byId('viz-btn-micro')?.addEventListener('click', () => {
836
+ currentVizMode = 'micro';
837
+ byId('viz-btn-micro').classList.add('on');
838
+ byId('viz-btn-macro').classList.remove('on');
839
+ byId('viz-btn-graph')?.classList.remove('on');
840
+ byId('visualizer-container').style.display = 'block';
841
+ const g = byId('visualizer-graph-container'); if (g) g.style.display = 'none';
842
+ if (currentSnapshotData) updateVisualizer(currentSnapshotData);
843
+ });
844
+
845
+ byId('viz-btn-graph')?.addEventListener('click', () => {
846
+ currentVizMode = 'graph';
847
+ byId('viz-btn-graph').classList.add('on');
848
+ byId('viz-btn-macro').classList.remove('on');
849
+ byId('viz-btn-micro').classList.remove('on');
850
+ byId('visualizer-container').style.display = 'none';
851
+ const g = byId('visualizer-graph-container'); if (g) g.style.display = 'block';
852
+ });
853
+
854
+ function renderRecommendedSkills(skills) {
855
+ const root = byId('recommended-skills-list');
856
+ if (!root) return;
857
+ root.replaceChildren();
858
+ const list = skills || [];
859
+ const installedCount = list.filter(s => s.installed).length;
860
+ pill('skills-count-pill', installedCount === list.length ? 'ok' : 'active', installedCount + '/' + list.length + ' Installed');
861
+
862
+ list.forEach(skill => {
863
+ const card = document.createElement('article');
864
+ card.className = 'skill-card ' + (skill.installed ? 'skill-installed' : 'skill-missing');
865
+ const catBadge = skill.category === 'vendor' ? '<span class="skill-cat vendor">VENDOR</span>' : '<span class="skill-cat skill">SKILL</span>';
866
+ const statusAction = skill.installed
867
+ ? '<span class="badge-installed">✔ Installed</span>'
868
+ : '<button class="btn-install-cmd" type="button" data-cmd="' + escapeHtml(skill.installCommand) + '">📋 Copy Command</button>';
869
+
870
+ card.innerHTML =
871
+ '<div class="skill-head">' +
872
+ '<div class="skill-title-block">' +
873
+ '<strong class="skill-title">' + escapeHtml(skill.title) + '</strong>' +
874
+ '<span class="skill-name"><code>' + escapeHtml(skill.name) + '</code></span>' +
875
+ '</div>' +
876
+ catBadge +
877
+ '</div>' +
878
+ '<p class="skill-desc">' + escapeHtml(skill.description) + '</p>' +
879
+ '<div class="skill-footer">' +
880
+ statusAction +
881
+ '</div>';
882
+
883
+ const btn = card.querySelector('.btn-install-cmd');
884
+ if (btn) {
885
+ wireCopy(btn, () => btn.getAttribute('data-cmd'));
886
+ }
887
+ root.append(card);
888
+ });
889
+ }
890
+
891
+ function renderMcp(tools) {
892
+ const root = byId('mcp-list'); if (!root) return; root.replaceChildren();
893
+ (tools || []).forEach(t => {
894
+ const card = document.createElement('div'); card.className = 'mcp-card';
895
+ card.innerHTML = '<strong>⚙️ ' + t.name + '</strong><div class="mcp-desc">' + t.description + '</div>';
896
+ root.append(card);
897
+ });
898
+ }
899
+
900
+ byId('graph-search-btn')?.addEventListener('click', async () => {
901
+ const input = byId('graph-file-input'); const res = byId('blast-result');
902
+ if (!input || !res) return;
903
+ const file = input.value.trim();
904
+ if (!file) return;
905
+ res.style.display = 'block'; res.innerHTML = 'Analyzing Blast Radius...';
906
+ try {
907
+ const resp = await fetch('/api/graph?file=' + encodeURIComponent(file));
908
+ const data = await resp.json();
909
+ if (data.error) { res.innerHTML = '<span style="color:var(--red)">' + data.error + '</span>'; return; }
910
+ let html = '<strong>Impact Score: <span style="color:var(--gold)">' + data.impactScore + '</span> | Total Affected: ' + data.totalAffected + ' files</strong><br/><br/>';
911
+ if (data.directDependents?.length) {
912
+ html += '<strong>Direct Dependents:</strong><ul>' + data.directDependents.map(d => '<li>' + d + '</li>').join('') + '</ul>';
913
+ }
914
+ if (data.transitiveDependents?.length) {
915
+ html += '<br/><strong>Transitive Dependents:</strong><ul>' + data.transitiveDependents.map(t => '<li>' + t + '</li>').join('') + '</ul>';
916
+ }
917
+ res.innerHTML = html;
918
+ } catch (e) {
919
+ res.innerHTML = '<span style="color:var(--red)">Error: ' + e.message + '</span>';
920
+ }
921
+ });
922
+
923
+ function renderActiveWorkspaces(activeRuns, work) {
924
+ const root = byId('active-workspaces-list');
925
+ if (!root) return;
926
+ root.replaceChildren();
927
+ const runs = (activeRuns && activeRuns.length > 0) ? activeRuns : (work && work.state === 'active' ? [{
928
+ runId: work.runId || 'ACTIVE',
929
+ title: work.title || 'Active Spec',
930
+ status: work.status || 'implementing',
931
+ branch: 'current',
932
+ totalTasks: work.total || 0,
933
+ completedTasks: work.completed || 0,
934
+ remainingTasks: work.remaining || 0,
935
+ hasOpenFindings: false
936
+ }] : []);
937
+
938
+ pill('active-workspaces-pill', runs.length > 0 ? 'active' : 'idle', runs.length + ' Active');
939
+ text('active-workspaces-summary', runs.length > 0 ? 'Found ' + runs.length + ' active workspace(s) in devflow/context/' : 'No active workspace folders. Run /feature or /fix to start.');
940
+
941
+ if (runs.length === 0) {
942
+ const empty = document.createElement('div');
943
+ empty.className = 'empty';
944
+ empty.style.padding = '18px';
945
+ empty.textContent = 'No active delivery runs in progress. Use /feature or /fix to create a living spec.';
946
+ root.append(empty);
947
+ return;
948
+ }
949
+
950
+ runs.forEach((run) => {
951
+ const card = document.createElement('article');
952
+ card.className = 'workspace-card';
953
+ const review = run.review || { state: 'none', freshness: 'not-applicable', verdict: null, checkResult: null };
954
+ const pct = run.totalTasks > 0 ? Math.round((run.completedTasks / run.totalTasks) * 100) : 0;
955
+ const stageClass = (run.status || '').includes('check') ? 'ws-stage stage-check' : 'ws-stage';
956
+ const findingTag = run.hasOpenFindings ? '<span style="color:var(--red);font-size:10px;font-weight:700;">⚠ Open Findings</span>' : '<span style="color:var(--mint);font-size:10px;font-weight:700;">✔ 0 Blockers</span>';
957
+
958
+ card.innerHTML = '<div class="ws-header">' +
959
+ '<span class="ws-id">' + escapeHtml(run.runId) + '</span>' +
960
+ '<span class="' + stageClass + '">Stage: ' + escapeHtml(run.status || 'active') + '</span>' +
961
+ '</div>' +
962
+ '<div class="ws-title">' + escapeHtml(run.title || 'Living Spec') + '</div>' +
963
+ '<div class="ws-progress-bar"><div class="ws-progress-fill" style="width:' + pct + '%;"></div></div>' +
964
+ '<div class="ws-meta">' +
965
+ '<span>🌿 <code>' + escapeHtml(run.branch || 'main') + '</code></span>' +
966
+ '<span><strong>' + pct + '%</strong> (' + run.completedTasks + '/' + run.totalTasks + ' Tasks)</span>' +
967
+ '</div>' +
968
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:2px;">' +
969
+ findingTag +
970
+ '<span style="font-size:10px;color:var(--muted);">' + run.remainingTasks + ' remaining</span>' +
971
+ '</div>' +
972
+ '<div style="font-size:10px;color:var(--muted);margin-top:6px;">Review: <strong>' + escapeHtml(review.verdict || review.state) + '</strong> · ' + escapeHtml(review.freshness) + '</div>' +
973
+ '<div class="ws-actions">' +
974
+ '<button class="btn-ws" type="button" data-cmd="/implement ' + escapeHtml(run.runId) + '">▶ /implement</button>' +
975
+ '<button class="btn-ws" type="button" data-cmd="/check ' + escapeHtml(run.runId) + '">🧪 /check</button>' +
976
+ '<button class="btn-ws" type="button" data-cmd="/complete ' + escapeHtml(run.runId) + '">📦 /complete</button>' +
977
+ '</div>';
978
+
979
+ card.querySelectorAll('.btn-ws').forEach(btn => {
980
+ wireCopy(btn, () => btn.getAttribute('data-cmd'));
981
+ });
982
+ root.append(card);
983
+ });
984
+ }
985
+
986
+ function renderSnapshot(data) {
987
+ const status = data.status || {}; const project = status.project || {}; const devflow = status.devflow || {}; const work = status.currentWork || {}; const git = status.git || {}; const findings = status.findings || { active: [], blockers: [] }; const review = status.review || { state: 'none', freshness: 'not-applicable', verdict: null, checkResult: null, warnings: [] }; const workflow = data.workflow || {}; const update = data.update || {}; const doctor = data.doctor || { checks: [] }; const discoveries = data.discoveries || { recent: [] }; const history = data.history || { items: [], total: 0 }; const ideas = status.ideas || { pending: [], totalPending: 0 }; const gate = data.gatekeeper || { passed: true }; const drift = data.drift || { hasDrift: false };
988
+
989
+ text('project-name', project.name || 'Nexus-DevFlow'); text('project-path', project.root || ''); text('version-badge', 'v' + (devflow.version || '2.5.0'));
990
+ pill('health-badge', status.health, status.health === 'ok' ? 'Health OK' : 'Health warning');
991
+ pill('gate-badge', gate.passed ? 'ok' : 'blocked', gate.passed ? '✔ Gate Passed' : '✖ Gate Blocked');
992
+ pill('drift-badge', !drift.hasDrift ? 'ok' : 'warning', !drift.hasDrift ? '✔ In Sync' : '⚠ Drift Detected');
993
+ pill('track-badge', workflow.track, 'Track ' + titleCase(workflow.track || 'idle'));
994
+
995
+ text('adapter-count', (devflow.adapters || []).length + ' configured'); text('header-branch', git.branch || 'unknown'); text('generated-at', new Date(data.generatedAt).toLocaleTimeString()); text('footer-revision', 'snapshot ' + data.generatedAt);
996
+ if (byId('pipeline-fast')) {
997
+ renderPipeline('pipeline-preflight', workflow.deep || workflow.preflight); renderPipeline('pipeline-fast', workflow.fast); decoratePipeline('pipeline-preflight'); decoratePipeline('pipeline-fast');
998
+ if (!userHasSwitchedTab) {
999
+ if (work.state === 'active' || workflow.track === 'fast') {
1000
+ selectTrack('fast');
1001
+ } else {
1002
+ selectTrack('preflight');
1003
+ }
1004
+ }
1005
+ }
1006
+ currentSnapshotData = data;
1007
+ renderRecommendedSkills(data.recommendedSkills);
1008
+ updateVisualizer(data);
1009
+ renderMcp(data.mcpTools); text('stat-mcp', (data.mcpTools || []).length + ' Ready');
1010
+ renderActiveWorkspaces(status.activeRuns, work);
1011
+
1012
+ const nextCommand = data.nextAction?.command || status.nextAction?.command || '/feature'; text('next-command', nextCommand); text('next-reason', data.nextAction?.reason || 'Ready for new work.');
1013
+ text('stat-runs', history.total || 0); text('stat-findings', (findings.active || []).length); text('stat-ideas', ideas.totalPending || 0); text('stat-state', titleCase(work.state || 'idle'));
1014
+ pill('update-pill', update.state, update.state === 'current' ? 'up to date' : update.state); text('installed-latest', (update.installedVersion || '2.5.0') + ' / ' + (update.latestVersion || '2.5.0')); text('system-health', status.health || 'ok');
1015
+ pill('work-pill', work.state, work.state || 'idle'); text('work-title', work.title || 'No active delivery run'); const pct = work.total > 0 ? Math.round((work.completed / work.total) * 100) : 0; byId('work-progress').style.width = pct + '%'; text('work-meta', work.state === 'active' ? work.completed + ' of ' + work.total + ' steps completed' : 'Ready');
1016
+ pill('git-pill', git.clean ? 'ok' : 'warning', git.clean ? 'clean' : (git.changedFiles || 0) + ' changed'); text('git-branch', git.branch || 'unknown'); text('git-changed', git.clean ? 'clean' : (git.changedFiles || 0) + ' changed files'); text('git-drift', drift.clean ? 'In Sync' : 'Drift detected'); text('git-commit', git.lastCommit || '-');
1017
+ text('findings-count', findings.total || 0); ['P0','P1','P2','P3'].forEach((severity) => text('sev-' + severity.toLowerCase(), (findings.active || []).filter((item) => item.severity === severity).length)); list('findings-list', (findings.active || []).map((item) => item.id + ' · ' + item.title), 'No active findings');
1018
+ pill('completion-pill', status.completion?.state, status.completion?.state || 'unknown'); list('completion-list', status.completion?.blockers || [], work.state === 'idle' ? 'No active run to complete' : 'All readiness checks passed'); text('warnings-count', (status.warnings || []).length); list('warnings-list', (status.warnings || []).map((item) => item.message), 'No active warnings or drift');
1019
+ pill('review-state', review.state, review.state); text('review-verdict', review.verdict || '-'); text('review-freshness', review.freshness); text('review-check', review.checkResult || '-'); list('review-warnings', (review.warnings || []).map((item) => item.message), 'No review warnings');
1020
+ pill('doctor-pill', doctor.failCount ? 'fail' : doctor.warnCount ? 'warning' : 'ok', doctor.passCount + '/' + doctor.totalChecks + ' pass'); renderDoctor(doctor.checks || []);
1021
+ text('discovery-count', discoveries.total || 0); renderDiscoveries(discoveries.recent || []); renderCommands(data.commands);
1022
+ text('ideas-count', (ideas.totalPending || 0) + ' pending'); renderIdeas(ideas.pending || []); text('history-count', (history.total || 0) + ' released'); renderHistory(history.items || []);
1023
+ }
1024
+
1025
+ window.__INITIAL_SNAPSHOT__ = null;
1026
+ if (window.__INITIAL_SNAPSHOT__) {
1027
+ try {
1028
+ renderSnapshot(window.__INITIAL_SNAPSHOT__);
1029
+ byId('live-dot').classList.add('on');
1030
+ text('live-label','Connected · live snapshot every 2 seconds');
1031
+ } catch (e) {
1032
+ console.error('Initial hydration error:', e);
1033
+ }
1034
+ }
1035
+
1036
+ let refreshing = false;
1037
+ async function refresh() { if (refreshing) return; refreshing = true; try { const response = await fetch('/api/dashboard'); if (!response.ok) throw new Error('HTTP ' + response.status); renderSnapshot(await response.json()); byId('live-dot').classList.add('on'); text('live-label','Connected · live snapshot every 2 seconds'); } catch (error) { byId('live-dot').classList.remove('on'); text('live-label','Disconnected · ' + error.message); } finally { refreshing = false; } }
1038
+ if (!window.__INITIAL_SNAPSHOT__) { refresh(); }
1039
+ setInterval(refresh, 2000);
1040
+ </script>
1041
+ </body>
1042
+ </html>`;
383
1043
  export class StudioViewRenderer {
384
1044
  defaultTheme;
385
1045
  constructor(options = {}) {
@@ -693,15 +1353,207 @@ export class StudioViewRenderer {
693
1353
  }
694
1354
  `.trim();
695
1355
  }
1356
+ /**
1357
+ * Resolves the active stage name from a dashboard snapshot.
1358
+ */
1359
+ resolveActiveStage(snapshot) {
1360
+ if (snapshot.workflow?.currentStage) {
1361
+ return snapshot.workflow.currentStage;
1362
+ }
1363
+ const primaryRun = snapshot.status?.activeRuns?.[0];
1364
+ if (primaryRun?.stage)
1365
+ return primaryRun.stage;
1366
+ if (primaryRun?.status)
1367
+ return primaryRun.status;
1368
+ if (snapshot.status?.currentWork?.status)
1369
+ return snapshot.status.currentWork.status;
1370
+ const nextCmd = snapshot.nextAction?.command || snapshot.status?.nextAction?.command;
1371
+ if (nextCmd) {
1372
+ if (nextCmd.includes("complete") || nextCmd.includes("merge"))
1373
+ return "complete";
1374
+ if (nextCmd.includes("check") || nextCmd.includes("verif") || nextCmd.includes("test"))
1375
+ return "check";
1376
+ if (nextCmd.includes("implement") || nextCmd.includes("build"))
1377
+ return "implement";
1378
+ if (nextCmd.includes("feature") || nextCmd.includes("spec"))
1379
+ return "feature";
1380
+ }
1381
+ return "feature";
1382
+ }
1383
+ /**
1384
+ * Renders the Macro Living Spec Lifecycle Pulse Map SVG.
1385
+ */
1386
+ renderMacroLifecycleSvg(activeStage = "feature") {
1387
+ const stageOrder = ["feature", "implement", "check", "complete"];
1388
+ const normalize = (raw) => {
1389
+ const s = String(raw || "").toLowerCase().trim();
1390
+ if (s.includes("complete") || s.includes("archive") || s.includes("merge") || s.includes("done"))
1391
+ return "complete";
1392
+ if (s.includes("check") || s.includes("verif") || s.includes("qa") || s.includes("test"))
1393
+ return "check";
1394
+ if (s.includes("implement") || s.includes("progress") || s.includes("build") || s.includes("code") || s.includes("dev"))
1395
+ return "implement";
1396
+ return "feature";
1397
+ };
1398
+ const activeId = normalize(activeStage);
1399
+ const activeIdx = stageOrder.indexOf(activeId);
1400
+ const stages = [
1401
+ { id: "feature", label: "/feature", desc: "Discover & Spec", icon: "🎯", x: 60 },
1402
+ { id: "implement", label: "/implement", desc: "TDD Build", icon: "⚙️", x: 270 },
1403
+ { id: "check", label: "/check", desc: "QA Proof", icon: "🧪", x: 480 },
1404
+ { id: "complete", label: "/complete", desc: "Archive & Merge", icon: "📦", x: 690 },
1405
+ ];
1406
+ let connectors = "";
1407
+ for (let i = 0; i < stages.length - 1; i++) {
1408
+ const from = stages[i];
1409
+ const to = stages[i + 1];
1410
+ const x1 = from.x + 130;
1411
+ const x2 = to.x;
1412
+ const y = 60;
1413
+ const isPastOrActive = i < activeIdx;
1414
+ const stroke = isPastOrActive ? "#7dd8ff" : "rgba(191, 224, 255, 0.25)";
1415
+ const traceClass = isPastOrActive ? "viz-active-trace" : "";
1416
+ connectors += `
1417
+ <line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${stroke}" stroke-width="${isPastOrActive ? 3 : 2}" class="${traceClass}" />
1418
+ <polygon points="${x2 - 6},${y - 4} ${x2},${y} ${x2 - 6},${y + 4}" fill="${stroke}" />
1419
+ `;
1420
+ }
1421
+ const nodes = stages.map((s, idx) => {
1422
+ const isDone = idx < activeIdx;
1423
+ const isActive = idx === activeIdx;
1424
+ let stroke = "rgba(191, 224, 255, 0.2)";
1425
+ let fill = "rgba(255, 255, 255, 0.02)";
1426
+ let labelFill = "#7598ba";
1427
+ let statusBadge = `<text x="${s.x + 120}" y="42" text-anchor="end" fill="#7598ba" font-family="var(--font-mono)" font-size="9">⏳ Next</text>`;
1428
+ let glowFilter = "";
1429
+ let activeClass = "";
1430
+ let pulseRing = "";
1431
+ if (isDone) {
1432
+ stroke = "#6fe3b4";
1433
+ fill = "rgba(111, 227, 180, 0.08)";
1434
+ labelFill = "#6fe3b4";
1435
+ statusBadge = `<text x="${s.x + 120}" y="42" text-anchor="end" fill="#6fe3b4" font-family="var(--font-mono)" font-size="9" font-weight="700">✔ Done</text>`;
1436
+ }
1437
+ else if (isActive) {
1438
+ stroke = "#7dd8ff";
1439
+ fill = "rgba(125, 216, 255, 0.16)";
1440
+ labelFill = "#edf6ff";
1441
+ statusBadge = `<text x="${s.x + 120}" y="42" text-anchor="end" fill="#7dd8ff" font-family="var(--font-mono)" font-size="9" font-weight="700">● Active</text>`;
1442
+ glowFilter = 'filter="url(#archify-glow)"';
1443
+ activeClass = "is-active";
1444
+ pulseRing = `<circle cx="${s.x + 24}" cy="60" r="18" fill="none" stroke="#7dd8ff" stroke-width="2" class="viz-pulse-circle" opacity="0.8"/>`;
1445
+ }
1446
+ return `
1447
+ <g class="viz-stage-node ${activeClass}">
1448
+ <rect x="${s.x}" y="25" width="130" height="70" rx="8" fill="${fill}" stroke="${stroke}" stroke-width="${isActive ? 2 : 1}" ${glowFilter} />
1449
+ ${pulseRing}
1450
+ <text x="${s.x + 16}" y="65" font-size="18">${s.icon}</text>
1451
+ <text x="${s.x + 44}" y="52" fill="${labelFill}" font-family="var(--font-mono)" font-size="12" font-weight="700">${s.label}</text>
1452
+ <text x="${s.x + 44}" y="72" fill="#7598ba" font-family="var(--font-sans)" font-size="10">${s.desc}</text>
1453
+ ${statusBadge}
1454
+ </g>
1455
+ `;
1456
+ }).join("");
1457
+ return `
1458
+ <svg viewBox="0 0 880 120" width="100%" height="120" xmlns="http://www.w3.org/2000/svg">
1459
+ <defs>
1460
+ <filter id="archify-glow" x="-20%" y="-20%" width="140%" height="140%">
1461
+ <feGaussianBlur stdDeviation="6" result="blur"/>
1462
+ <feComposite in="SourceGraphic" in2="blur" operator="over"/>
1463
+ </filter>
1464
+ </defs>
1465
+ ${connectors}
1466
+ ${nodes}
1467
+ </svg>
1468
+ `.trim();
1469
+ }
1470
+ /**
1471
+ * Renders the Micro Tracer-Bullet Tickets Dependency DAG SVG.
1472
+ */
1473
+ renderTicketsDagSvg(tickets = []) {
1474
+ if (!tickets || tickets.length === 0) {
1475
+ return `
1476
+ <svg viewBox="0 0 880 120" width="100%" height="120" xmlns="http://www.w3.org/2000/svg">
1477
+ <rect x="20" y="20" width="840" height="80" rx="8" fill="rgba(255,255,255,0.02)" stroke="rgba(191,224,255,0.15)" stroke-dasharray="4 4" />
1478
+ <text x="440" y="66" text-anchor="middle" fill="#7598ba" font-family="var(--font-mono)" font-size="12">
1479
+ No active tracer-bullet tickets found in devflow/context/{xxx-slug}/tickets/.
1480
+ </text>
1481
+ </svg>
1482
+ `.trim();
1483
+ }
1484
+ const cardWidth = 170;
1485
+ const cardHeight = 65;
1486
+ const gapX = 50;
1487
+ const startX = 40;
1488
+ const startY = 30;
1489
+ const positions = new Map();
1490
+ tickets.forEach((t, i) => {
1491
+ positions.set(t.id, { x: startX + i * (cardWidth + gapX), y: startY });
1492
+ });
1493
+ const totalWidth = Math.max(880, startX * 2 + tickets.length * (cardWidth + gapX));
1494
+ let connectors = "";
1495
+ tickets.forEach((ticket) => {
1496
+ const toPos = positions.get(ticket.id);
1497
+ if (!toPos)
1498
+ return;
1499
+ (ticket.blockedBy || []).forEach((blockerId) => {
1500
+ const fromPos = positions.get(blockerId);
1501
+ if (!fromPos)
1502
+ return;
1503
+ const x1 = fromPos.x + cardWidth;
1504
+ const y1 = fromPos.y + cardHeight / 2;
1505
+ const x2 = toPos.x;
1506
+ const y2 = toPos.y + cardHeight / 2;
1507
+ connectors += `
1508
+ <line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#7dd8ff" stroke-width="2" stroke-dasharray="4 4" marker-end="url(#arrow)" />
1509
+ `;
1510
+ });
1511
+ });
1512
+ const cards = tickets.map((t) => {
1513
+ const pos = positions.get(t.id) || { x: 0, y: 0 };
1514
+ const statusColor = t.status === "done" ? "#6fe3b4" : t.status === "in_progress" ? "#7dd8ff" : "#ff7061";
1515
+ const statusBg = t.status === "done" ? "rgba(111, 227, 180, 0.12)" : t.status === "in_progress" ? "rgba(125, 216, 255, 0.15)" : "rgba(255, 112, 97, 0.12)";
1516
+ const statusLabel = t.status === "done" ? "✔ Done" : t.status === "in_progress" ? "▶ Active" : "⏳ Blocked";
1517
+ const titleEsc = this.escapeHtml(t.title.length > 20 ? t.title.slice(0, 19) + "…" : t.title);
1518
+ return `
1519
+ <g class="viz-ticket-node ticket-${t.status}">
1520
+ <rect x="${pos.x}" y="${pos.y}" width="${cardWidth}" height="${cardHeight}" rx="6" fill="${statusBg}" stroke="${statusColor}" stroke-width="1.5" />
1521
+ <text x="${pos.x + 12}" y="${pos.y + 22}" fill="${statusColor}" font-family="var(--font-mono)" font-size="11" font-weight="700">${this.escapeHtml(t.id)}</text>
1522
+ <text x="${pos.x + cardWidth - 12}" y="${pos.y + 22}" text-anchor="end" fill="${statusColor}" font-family="var(--font-mono)" font-size="9" font-weight="600">${statusLabel}</text>
1523
+ <text x="${pos.x + 12}" y="${pos.y + 44}" fill="#edf6ff" font-family="var(--font-sans)" font-size="11" font-weight="500">${titleEsc}</text>
1524
+ </g>
1525
+ `;
1526
+ }).join("");
1527
+ return `
1528
+ <svg viewBox="0 0 ${totalWidth} 130" width="100%" height="130" xmlns="http://www.w3.org/2000/svg">
1529
+ <defs>
1530
+ <marker id="arrow" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
1531
+ <path d="M 0 1 L 8 5 L 0 9 z" fill="#7dd8ff"/>
1532
+ </marker>
1533
+ </defs>
1534
+ ${connectors}
1535
+ ${cards}
1536
+ </svg>
1537
+ `.trim();
1538
+ }
1539
+ /**
1540
+ * Returns the static base dashboard HTML without snapshot injection.
1541
+ */
1542
+ getBaseDashboardHtml() {
1543
+ return this.renderWebDashboard();
1544
+ }
696
1545
  /**
697
1546
  * Renders the complete web dashboard HTML document with optional initial snapshot injection.
698
1547
  */
699
1548
  renderWebDashboard(snapshot) {
1549
+ const activeStage = snapshot ? this.resolveActiveStage(snapshot) : "feature";
1550
+ const initialSvg = this.renderMacroLifecycleSvg(activeStage);
1551
+ let html = DASHBOARD_BASE_HTML.replace('<div class="visualizer-canvas-container" id="visualizer-container"></div>', `<div class="visualizer-canvas-container" id="visualizer-container">${initialSvg}</div>`);
700
1552
  if (!snapshot) {
701
- return DASHBOARD_PAGE_HTML;
1553
+ return html;
702
1554
  }
703
1555
  const serialized = this.escapeJsonForHtml(snapshot);
704
- return DASHBOARD_PAGE_HTML.replace("window.__INITIAL_SNAPSHOT__ = null;", `window.__INITIAL_SNAPSHOT__ = ${serialized};`);
1556
+ return html.replace("window.__INITIAL_SNAPSHOT__ = null;", `window.__INITIAL_SNAPSHOT__ = ${serialized};`);
705
1557
  }
706
1558
  /**
707
1559
  * Renders the complete Webview Studio HTML document with VS Code bridge scripts.