agentic-workflow-manager 8.2.1 → 8.4.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 (39) hide show
  1. package/dist/src/commands/doctor.js +40 -1
  2. package/dist/src/commands/hooks/claude.js +47 -31
  3. package/dist/src/commands/hooks/index.js +1 -1
  4. package/dist/src/commands/hooks/shared.js +1 -1
  5. package/dist/src/commands/registry/add.js +6 -1
  6. package/dist/src/commands/registry/index.js +3 -0
  7. package/dist/src/core/context/orchestrator.js +15 -2
  8. package/dist/src/core/context/provider.js +29 -1
  9. package/dist/src/core/context/regenerate.js +29 -0
  10. package/dist/src/core/dashboard/collect.js +170 -0
  11. package/dist/src/core/dashboard/plan-state.js +44 -0
  12. package/dist/src/core/dashboard/render-html.js +82 -0
  13. package/dist/src/core/dashboard/render-terminal.js +43 -0
  14. package/dist/src/core/dashboard/sanitize.js +62 -0
  15. package/dist/src/core/dashboard/styles.js +51 -0
  16. package/dist/src/core/dashboard/types.js +21 -0
  17. package/dist/src/core/dashboard/validate.js +106 -0
  18. package/dist/src/core/dashboard/write-html.js +88 -0
  19. package/dist/src/core/diagnostics/context.js +1 -1
  20. package/dist/src/core/orchestrators.js +142 -0
  21. package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
  22. package/dist/tests/commands/doctor.test.js +160 -0
  23. package/dist/tests/commands/hooks/install-symlink-fallback.test.js +13 -4
  24. package/dist/tests/commands/hooks/install.test.js +121 -3
  25. package/dist/tests/commands/hooks/resync.test.js +40 -2
  26. package/dist/tests/commands/registry/add.test.js +104 -0
  27. package/dist/tests/core/context/orchestrator.test.js +86 -0
  28. package/dist/tests/core/context/provider.test.js +137 -0
  29. package/dist/tests/core/context/regenerate.test.js +56 -0
  30. package/dist/tests/core/dashboard/collect.test.js +173 -0
  31. package/dist/tests/core/dashboard/contracts.test.js +92 -0
  32. package/dist/tests/core/dashboard/plan-state.test.js +32 -0
  33. package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
  34. package/dist/tests/core/dashboard/render-html.test.js +175 -0
  35. package/dist/tests/core/dashboard/render-terminal.test.js +65 -0
  36. package/dist/tests/core/dashboard/write-html.test.js +112 -0
  37. package/dist/tests/core/orchestrators.test.js +236 -0
  38. package/dist/tests/helpers/dashboard-fixtures.js +66 -0
  39. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDashboardHtml = renderDashboardHtml;
4
+ const styles_1 = require("./styles");
5
+ const validate_1 = require("./validate");
6
+ const CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
7
+ const SECTION_TITLES = { machine: 'Machine / install', project: 'Project readiness', planning: 'Design / planning', execution: 'Execution', qa: 'QA', retro: 'Retro', history: 'Final / history' };
8
+ const STATE_TEXT = { ok: 'OK', attention: 'Attention', missing: 'Missing', unavailable: 'Unavailable', not_applicable: 'Not applicable' };
9
+ const STATE_GLYPH = { ok: '●', attention: '▲', missing: '×', unavailable: '⊘', not_applicable: '—' };
10
+ function escapeHtml(value) { return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
11
+ function sectionHtml(section, supplement = '') {
12
+ const title = SECTION_TITLES[section.id];
13
+ const availability = section.availability === 'available' ? '' : `<p class="availability ${section.availability}">⊘ Source ${escapeHtml(section.availability.replace('_', ' '))}</p>`;
14
+ const rows = section.items.length === 0 ? '<p class="empty">No observations reported.</p>' : `<table><thead><tr><th scope="col">Observation</th><th scope="col">State</th><th scope="col">Detail</th><th scope="col">Remediation</th></tr></thead><tbody>${section.items.map((item) => `<tr><td data-label="Observation">${escapeHtml(item.label)}</td><td data-label="State"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span></td><td data-label="Detail">${item.detail ? escapeHtml(item.detail) : '—'}</td><td data-label="Remediation">${item.remediation ? `<code>${escapeHtml(item.remediation)}</code>` : '—'}</td></tr>`).join('')}</tbody></table>`;
15
+ return `<section id="${section.id}" aria-label="${title}"><header><h2>${title}</h2><span class="eyebrow">${escapeHtml(section.availability.replace('_', ' '))}</span></header><div class="section-body">${availability}${supplement}${rows}</div></section>`;
16
+ }
17
+ function diagnosticCards(items, attribute) {
18
+ const labels = ['installation', 'sensors', 'permissions'];
19
+ const cards = items.slice(0, 3).map((item, index) => `<li data-diagnostic-card="${labels[index] ?? 'diagnostic'}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'No additional detail'}</span></li>`).join('') || '<li><span class="state not_applicable">— Not applicable</span><strong>No machine observations</strong><span>Machine diagnostics are not available.</span></li>';
20
+ if (attribute === 'data-machine-diagnostics')
21
+ return `<section data-machine-diagnostics aria-labelledby="machine-diagnostics-heading"><header><h2 id="machine-diagnostics-heading">Machine diagnostics</h2></header><div class="section-body"><ul class="diagnostic-grid">${cards}</ul></div></section>`;
22
+ return `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${cards}</ul></div>`;
23
+ }
24
+ function machineBento(items) {
25
+ const names = ['Instalación', 'Sensores globales', 'Persistencia'];
26
+ return `<div class="machine-bento" data-machine-bento>${names.map((name, index) => {
27
+ const item = items[index];
28
+ return `<article class="bento-card"><h2>${name}</h2>${item ? `<span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'Sin detalle adicional'}</span>` : '<span class="state unavailable">⊘ Unavailable</span><span>Sin observación disponible</span>'}</article>`;
29
+ }).join('')}</div>`;
30
+ }
31
+ function nextActions(snapshot) {
32
+ const actionable = snapshot.sections.flatMap((section) => section.items.filter((item) => item.remediation)).slice(0, 2);
33
+ const rows = [...actionable, { id: 'setup.initialize', label: 'Inicializar proyecto', state: 'attention', remediation: 'awm init' }];
34
+ return `<div data-next-actions role="group" aria-labelledby="next-actions-heading"><h2 id="next-actions-heading">Siguiente acción requerida</h2><ol class="action-list">${rows.map((item) => `<li data-next-action="${escapeHtml(item.id)}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><span>${escapeHtml(item.label)}</span><code>${item.remediation}</code><button type="button" disabled aria-describedby="static-controls-note">Copy command (static)</button></li>`).join('')}</ol></div>`;
35
+ }
36
+ function privacyAndActions(snapshot) {
37
+ return `<section data-privacy-security aria-label="Privacy and security"><header><h2>Privacy &amp; security</h2></header><div class="section-body privacy-body"><div><p class="lede">This portable view contains sanitized states and exact operator remedies only. It excludes paths, identities, environment values, secret-like values, raw command output, ledger prose, and error stacks.</p><p id="privacy-toggle-note" class="static-note">Static export: this checked setting documents the enforced share-safe boundary and cannot be changed in this file.</p></div><label class="static-toggle" data-static-privacy-toggle><span>Share-safe sanitization</span><input type="checkbox" checked disabled aria-describedby="privacy-toggle-note"><span aria-hidden="true">Enabled</span></label></div></section>${nextActions(snapshot)}`;
38
+ }
39
+ function dashboardToolbar() {
40
+ return `<header class="dashboard-toolbar" data-dashboard-toolbar><p class="toolbar-brand">AWM <span>Doctor dashboard</span></p><form role="search" aria-label="Search dashboard"><label class="sr-only" for="dashboard-search">Search resources</label><input id="dashboard-search" type="search" placeholder="Search resources" disabled aria-describedby="static-controls-note"></form><div class="toolbar-actions"><button type="button" disabled aria-describedby="static-controls-note">Notifications (static)</button><button type="button" disabled aria-describedby="static-controls-note">Help (static)</button><button type="button" disabled aria-describedby="static-controls-note">Export dashboard (static)</button><button type="button" disabled aria-describedby="static-controls-note">New deployment (static)</button></div><p id="static-controls-note" class="sr-only">Controls are shown for reference only; this exported dashboard does not run scripts.</p></header>`;
41
+ }
42
+ function projectHeaderActions() {
43
+ return `<div class="project-header-actions" data-project-header-actions role="group" aria-label="Project actions"><button type="button" disabled aria-describedby="static-controls-note">Export evidence (static)</button><button type="button" disabled aria-describedby="static-controls-note">Attach evidence (static)</button></div>`;
44
+ }
45
+ function projectChrome(project) {
46
+ return `<div class="project-chrome"><p class="eyebrow" data-project-breadcrumb>AWM / Proyecto / ${project}</p><nav data-project-nav aria-label="Project navigation"><span aria-current="page">Proyecto</span><span>Ejecución</span><span>Configuración</span></nav></div>`;
47
+ }
48
+ function projectEvidenceComposition() {
49
+ return `<div data-project-evidence role="group" aria-labelledby="project-evidence-heading"><h3 id="project-evidence-heading" class="sr-only">Project evidence</h3><div class="evidence-grid compact-composition"><div><h3>Planes de trabajo</h3><div class="plan-card active" data-plan-card="active"><div><strong>Plan activo</strong><span class="state attention">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div><div class="plan-card blocked" data-plan-card="blocked"><div><strong>Plan bloqueado</strong><span class="state unavailable">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div></div><div><h3>Impacto y trazabilidad</h3><table class="evidence-table"><thead><tr><th scope="col">Tipo</th><th scope="col">Fuente</th><th scope="col">Estado</th><th scope="col">Fecha</th></tr></thead><tbody><tr><td colspan="4" class="empty">No hay evidencia de impacto disponible en este snapshot.</td></tr></tbody></table></div></div><div class="closure-actions" data-closure-actions role="group" aria-labelledby="closure-actions-heading"><div><h3 id="closure-actions-heading">Qué falta para cerrar el ciclo</h3><p class="empty">La ausencia de marcador no equivale a cero hallazgos.</p></div><div><button type="button" disabled aria-describedby="static-controls-note">Completar QA (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Registrar retro (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Adjuntar evidencia (estático)</button></div></div></div>`;
50
+ }
51
+ function projectComposition(snapshot) {
52
+ const stageSections = ['planning', 'execution', 'qa', 'retro', 'history'];
53
+ const byId = new Map(snapshot.sections.map((section) => [section.id, section]));
54
+ const stages = stageSections.map((id) => {
55
+ const section = byId.get(id);
56
+ const stage = id === 'history' ? 'evidence' : id;
57
+ const available = section?.availability === 'available';
58
+ return `<li data-stage="${stage}"><span aria-hidden="true" class="timeline-marker"></span><strong>${stage === 'evidence' ? 'Evidence' : SECTION_TITLES[id]}</strong><span class="state ${available ? 'ok' : 'unavailable'}">${available ? '● Available' : '⊘ Unavailable'}</span></li>`;
59
+ }).join('');
60
+ const provisional = snapshot.confidence === 'provisional' ? '<aside data-provisional-evidence aria-label="Provisional evidence"><strong>Provisional evidence</strong><span>Current observations are still being verified by downstream QA and evidence capture.</span></aside>' : '';
61
+ const prepItems = byId.get('machine')?.items ?? [];
62
+ const prepNames = ['installation', 'sensors', 'persistence'];
63
+ const preparation = `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${prepNames.map((name, index) => { const item = prepItems[index]; return `<li data-machine-preparation-card="${name}">${item ? `<span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'Sin detalle adicional'}</span>` : '<span class="state unavailable">⊘ Unavailable</span><strong>Sin observación</strong><span>Fuente no disponible</span>'}</li>`; }).join('')}</ul></div>`;
64
+ const machineSupplement = `${preparation}<nav class="lifecycle-timeline" data-lifecycle-timeline aria-labelledby="lifecycle-timeline-heading"><h3 id="lifecycle-timeline-heading">Línea de ciclo</h3><ol class="timeline connected-timeline">${stages}</ol></nav>${provisional}`;
65
+ const historySupplement = projectEvidenceComposition();
66
+ return snapshot.sections.map((section) => sectionHtml(section, section.id === 'machine' ? machineSupplement : section.id === 'history' ? historySupplement : '')).join('');
67
+ }
68
+ /** Renders a portable, static, share-safe dashboard document. */
69
+ function renderDashboardHtml(input) {
70
+ const snapshot = (0, validate_1.validateDashboardSnapshotV1)(input);
71
+ const overall = escapeHtml(snapshot.overall);
72
+ const project = escapeHtml(snapshot.project.label);
73
+ const machineItems = snapshot.sections.find((section) => section.id === 'machine')?.items ?? [];
74
+ const sections = snapshot.project.detected ? projectComposition(snapshot) : `${machineBento(machineItems)}${privacyAndActions(snapshot)}${snapshot.sections.map((section) => sectionHtml(section)).join('')}`;
75
+ const links = '<li>Inicio</li><li>Estado</li><li class="active">Configuración</li><li>Terminal</li>';
76
+ const projectDetected = snapshot.project.detected;
77
+ const heading = projectDetected ? 'Project lifecycle' : 'Machine configuration';
78
+ const intro = projectDetected ? 'Readiness, lifecycle state, and eligible observations presented directly for operator review.' : 'Machine readiness and safe configuration state outside a project.';
79
+ const navLabel = projectDetected ? 'Dashboard sections' : 'Machine configuration sections';
80
+ const context = projectDetected ? `Project: ${project}` : 'No project detected';
81
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="Content-Security-Policy" content="${CSP}"><title>AWM Doctor dashboard</title><style>${styles_1.DASHBOARD_STYLES}</style></head><body><div class="shell"><nav data-operational-sidebar aria-label="${navLabel}"><p class="brand">AWM<small>Doctor dashboard</small></p><ul>${links}</ul><button data-sidebar-diagnose type="button" disabled aria-describedby="static-controls-note">Ejecutar diagnóstico</button><div data-sidebar-operator-controls><span>Operaciones</span><button type="button" disabled aria-describedby="static-controls-note">Soporte</button><button type="button" disabled aria-describedby="static-controls-note">Cerrar sesión</button></div></nav><main>${dashboardToolbar()}<div class="dashboard-content"><header class="page-header">${projectDetected ? projectChrome(project) : ''}<p class="eyebrow">Read-only diagnostic evidence</p><h1>${heading}</h1><p class="lede">${intro}</p><p><span class="status ${overall}">● ${overall}</span> <span class="status">Confidence: ${escapeHtml(snapshot.confidence)}</span></p><p class="eyebrow">${context}</p>${projectDetected ? projectHeaderActions() : ''}</header>${sections}<footer>Generated ${escapeHtml(snapshot.generatedAt)} · Static share-safe dashboard</footer></div></main></div></body></html>\n`;
82
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderFullTerminal = renderFullTerminal;
4
+ const validate_1 = require("./validate");
5
+ const SECTION_TITLES = {
6
+ machine: 'Machine / install',
7
+ project: 'Project readiness',
8
+ planning: 'Design / planning',
9
+ execution: 'Execution',
10
+ qa: 'QA',
11
+ retro: 'Retro',
12
+ history: 'Final / history',
13
+ };
14
+ const STATE_PRESENTATION = {
15
+ ok: { glyph: '✔', text: 'ok' },
16
+ attention: { glyph: '⚠', text: 'attention' },
17
+ missing: { glyph: '✖', text: 'missing' },
18
+ unavailable: { glyph: '⊘', text: 'unavailable' },
19
+ not_applicable: { glyph: '—', text: 'not applicable' },
20
+ };
21
+ /** Renders a complete, color-free dashboard suitable for terminals and logs. */
22
+ function renderFullTerminal(input) {
23
+ const snapshot = (0, validate_1.validateDashboardSnapshotV1)(input);
24
+ const lines = [
25
+ `AWM dashboard · ${snapshot.overall}`,
26
+ `Project: ${snapshot.project.label}`,
27
+ `Confidence: ${snapshot.confidence}`,
28
+ ];
29
+ for (const section of snapshot.sections) {
30
+ lines.push('', SECTION_TITLES[section.id]);
31
+ if (section.availability !== 'available')
32
+ lines.push(` ⊘ source ${section.availability.replace('_', ' ')}`);
33
+ if (section.items.length === 0)
34
+ lines.push(' No observations reported.');
35
+ for (const item of section.items) {
36
+ const presentation = STATE_PRESENTATION[item.state];
37
+ lines.push(` ${presentation.glyph} ${item.label} [${item.id}]${item.detail ? ` — ${item.detail}` : ` — ${presentation.text}`}`);
38
+ if (item.remediation)
39
+ lines.push(` → ${item.remediation}`);
40
+ }
41
+ }
42
+ return lines.join('\n');
43
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeDashboardSource = sanitizeDashboardSource;
4
+ const crypto_1 = require("crypto");
5
+ const STATES = new Set(['ok', 'attention', 'missing', 'unavailable', 'not_applicable', 'active', 'blocked']);
6
+ const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'retroComplete']);
7
+ const CANONICAL_LABELS = new Set([
8
+ 'Preferences', 'Registries', 'Profile', 'Sensors', 'Optional source unavailable',
9
+ 'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight',
10
+ ]);
11
+ const CANONICAL_FINDING_IDS = new Set([
12
+ 'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
13
+ 'project.sensors.unavailable', 'project.preflight.degraded', 'planning.source.unavailable', 'execution.source.unavailable',
14
+ ]);
15
+ const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
16
+ const PROJECT_FINDING_ID = /^project\.(?:profile\.present|extensions\.configured|registry-pins\.present|bundles\.coherent|context\.present|constitution\.present|sensors\.present|preflight\.not_collected)$/;
17
+ const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
18
+ const DANGEROUS = /(?:ghp_|sk-[A-Za-z]|<|>|\\\\[^\\\s]+\\[^\\\s]+|\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*|[A-Za-z]:\\|\b[A-Za-z_][A-Za-z0-9_]*=|token|secret|password)/iu;
19
+ function sanitize(value, key) {
20
+ if (value === null || typeof value === 'boolean')
21
+ return value;
22
+ if (typeof value === 'number') {
23
+ if (!Number.isFinite(value))
24
+ throw new Error('Dashboard source numbers must be finite');
25
+ return value;
26
+ }
27
+ if (typeof value === 'string') {
28
+ if (key === 'state' && !STATES.has(value))
29
+ throw new Error(`Dashboard source state is invalid: ${value}`);
30
+ // IDs are source-controlled and are rendered into snapshots. Preserve only
31
+ // the small canonical vocabulary; opaque IDs retain deterministic ordering
32
+ // without exporting repository names, emails, IPs, or local identifiers.
33
+ if (key === 'id') {
34
+ if (value.trim() === '')
35
+ throw new Error('Dashboard finding id is invalid');
36
+ return CANONICAL_FINDING_IDS.has(value) || PROVIDER_FINDING_ID.test(value) || PROJECT_FINDING_ID.test(value)
37
+ ? value : `item-${(0, crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 16)}`;
38
+ }
39
+ if (key === 'label' && !CANONICAL_LABELS.has(value) && !PROVIDER_LABEL.test(value))
40
+ return '[redacted]';
41
+ return DANGEROUS.test(value) ? '[redacted]' : value;
42
+ }
43
+ if (Array.isArray(value))
44
+ return value.map((entry) => sanitize(entry));
45
+ if (!value || typeof value !== 'object')
46
+ throw new Error('Dashboard source must contain JSON-compatible values');
47
+ const out = {};
48
+ for (const [entryKey, entryValue] of Object.entries(value)) {
49
+ if (!ALLOWED_KEYS.has(entryKey))
50
+ continue;
51
+ // Source details are untrusted command/error output. Renderers only receive
52
+ // canonical details produced after collection (for example lifecycle state).
53
+ if (entryKey === 'detail')
54
+ continue;
55
+ out[entryKey] = sanitize(entryValue, entryKey);
56
+ }
57
+ return out;
58
+ }
59
+ /** Removes dynamic credentials, local paths, and hostile markup from source observations. */
60
+ function sanitizeDashboardSource(value) {
61
+ return sanitize(value);
62
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DASHBOARD_STYLES = void 0;
4
+ /** Inline visual system derived from the approved Stitch dashboard artifacts. */
5
+ exports.DASHBOARD_STYLES = `
6
+ :root { color-scheme: dark; --canvas:#070d16; --surface:#101a29; --surface-raised:#172235; --surface-nav:#202a3b; --ink:#edf3ff; --muted:#b7c2d4; --border:#39455b; --indigo:#aebcff; --cyan:#8cecff; --amber:#ffc06a; --red:#ffb4ac; --green:#77e8be; --radius:4px; }
7
+ * { box-sizing:border-box; }
8
+ html { background:var(--canvas); color:var(--ink); font:14px/1.35 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
9
+ body { margin:0; background:var(--canvas); }
10
+ .shell { display:grid; grid-template-columns:12rem minmax(0,1fr); min-height:100vh; }
11
+ .shell > nav { background:var(--surface-nav); border-right:1px solid var(--border); padding:.85rem .65rem; }
12
+ .brand { margin:0 0:1rem; font-size:.9rem; font-weight:750; letter-spacing:-.03em; }
13
+ .brand small { display:block; color:var(--muted); font-size:.72rem; font-weight:500; letter-spacing:0; }
14
+ .shell > nav ul { list-style:none; margin:0; padding:0; display:grid; gap:.15rem; }
15
+ .shell > nav li { color:var(--muted); font-size:.78rem; padding:.4rem .45rem; border-left:2px solid transparent; } .shell > nav li.active { background:var(--indigo); color:#101a29; font-weight:750; }
16
+ .shell > nav > button { margin-top:1rem; width:100%; } [data-sidebar-operator-controls] { border-top:1px solid var(--border); color:var(--muted); display:grid; font-size:.72rem; gap:.35rem; margin-top:1rem; padding-top:.75rem; }
17
+ main { min-width:0; padding:0 clamp(.75rem,3vw,2.5rem) 3rem; }
18
+ .dashboard-content { margin:0 auto; width:min(100%,72rem); }
19
+ header { border-bottom:1px solid var(--border); padding-bottom:1.25rem; }
20
+ .dashboard-toolbar { align-items:center; display:flex; flex-wrap:wrap; gap:.45rem; justify-content:space-between; margin:0 calc(clamp(.75rem,3vw,2.5rem) * -1); min-height:2.8rem; padding:.4rem clamp(.75rem,3vw,2.5rem); }
21
+ .toolbar-brand { font-weight:750; letter-spacing:-.025em; margin:0; } .toolbar-brand span { color:var(--muted); font-size:.76rem; font-weight:500; margin-left:.35rem; }
22
+ .dashboard-toolbar form { margin-left:auto; } .dashboard-toolbar input { background:#080e18; border:1px solid var(--border); border-radius:var(--radius); color:var(--muted); min-width:12rem; padding:.4rem .55rem; } .dashboard-toolbar input:disabled { cursor:not-allowed; opacity:.72; }
23
+ .toolbar-actions,.project-header-actions,.closure-actions > div:last-child { display:flex; flex-wrap:wrap; gap:.4rem; }
24
+ button { background:var(--surface-raised); border:1px solid var(--border); border-radius:var(--radius); color:var(--ink); font:inherit; padding:.38rem .55rem; } button:disabled { cursor:not-allowed; opacity:.7; } button:focus-visible,input:focus-visible { outline:3px solid var(--cyan); outline-offset:3px; }
25
+ .page-header { padding:1.35rem 0 .8rem; } .project-header-actions { margin-top:.6rem; }
26
+ h1,h2,p { margin-top:0; } h1 { font-size:clamp(1.45rem,2.4vw,2rem); letter-spacing:-.035em; margin-bottom:.2rem; } h2 { font-size:.88rem; letter-spacing:.035em; margin:0; } h3 { font-size:.8rem; margin:.1rem 0 .45rem; }
27
+ .eyebrow,.status { color:var(--muted); font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.78rem; }
28
+ .status { display:inline-flex; align-items:center; gap:.4rem; border:1px solid var(--border); border-radius:var(--radius); padding:.2rem .5rem; }
29
+ .status.degraded { color:var(--amber); border-color:#72552e; } .status.healthy { color:var(--green); border-color:#2f6c58; }
30
+ .lede { color:var(--muted); max-width:72ch; }
31
+ section { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); margin-top:.65rem; overflow:hidden; }
32
+ section > header { align-items:center; background:var(--surface-raised); border:0; display:flex; justify-content:space-between; padding:.55rem .75rem; }
33
+ .section-body { padding:.75rem; }
34
+ .availability { color:var(--muted); font-size:.85rem; margin-bottom:.75rem; } .availability.unavailable { color:var(--amber); }
35
+ table { border-collapse:collapse; width:100%; } th,td { border-bottom:1px solid var(--border); padding:.45rem .4rem; text-align:left; vertical-align:top; } th { color:var(--muted); font-size:.66rem; letter-spacing:.07em; text-transform:uppercase; } tr:last-child td { border-bottom:0; }
36
+ .state { font-weight:650; white-space:nowrap; } .state.ok { color:var(--green); } .state.attention { color:var(--amber); } .state.missing,.state.unavailable { color:var(--red); } .state.not_applicable { color:var(--muted); }
37
+ code { background:#080e18; border:1px solid var(--border); border-radius:2px; color:var(--cyan); font:inherit; padding:.12rem .3rem; white-space:pre-wrap; overflow-wrap:anywhere; }
38
+ .empty { color:var(--muted); margin:0; } footer { color:var(--muted); font-size:.8rem; padding:1.25rem 0; }
39
+ .diagnostic-grid,.timeline,.action-list { display:grid; gap:.45rem; list-style:none; margin:0; padding:0; } .diagnostic-grid { grid-template-columns:repeat(3,minmax(0,1fr)); } .machine-preparation-strip { background:#0b1320; border:1px solid var(--border); margin-bottom:.65rem; padding:.6rem; } .machine-preparation-strip h3 { color:var(--muted); font-size:.68rem; letter-spacing:.07em; text-transform:uppercase; } .diagnostic-grid li { border-right:1px solid var(--border); display:grid; gap:.2rem; padding:0 .5rem; } .diagnostic-grid li:last-child { border-right:0; } .diagnostic-grid strong { font-size:.78rem; } .diagnostic-grid li > span:last-child { color:var(--muted); font: .7rem ui-monospace,SFMono-Regular,Consolas,monospace; }
40
+ .lifecycle-timeline { background:#0b1320; border:1px solid var(--border); margin-bottom:.65rem; padding:.6rem; } .lifecycle-timeline h3 { color:var(--muted); font-size:.68rem; letter-spacing:.07em; text-transform:uppercase; } .timeline { grid-template-columns:repeat(5,minmax(0,1fr)); } .connected-timeline { border-top:1px solid var(--border); margin-top:.4rem; padding-top:.25rem; } .timeline li { background:#0b1320; display:grid; gap:.2rem; padding:.2rem; } .timeline-marker { background:var(--surface-raised); border:1px solid var(--border); border-radius:50%; height:.65rem; width:.65rem; } .timeline .state { font-size:.68rem; } .timeline .state.unavailable { background:#38141c; color:#fff0ed; display:inline-flex; padding:.08rem .25rem; width:max-content; } aside[data-provisional-evidence] { background:#261d10; border-left:3px solid var(--amber); color:var(--ink); display:grid; gap:.15rem; margin-top:.65rem; padding:.6rem .75rem; } aside[data-provisional-evidence] span { color:var(--muted); }
41
+ .privacy-body { align-items:center; display:flex; gap:1rem; justify-content:space-between; } .static-note { color:var(--muted); font-size:.78rem; margin:0; } .static-toggle { align-items:center; background:#0b1320; border:1px solid var(--border); display:flex; flex-wrap:wrap; gap:.5rem; padding:.65rem; } .static-toggle input { accent-color:var(--cyan); } .static-toggle input:disabled { opacity:1; }
42
+ .machine-bento { display:grid; gap:.65rem; grid-template-columns:repeat(3,minmax(0,1fr)); margin-top:.65rem; } .bento-card { background:var(--surface); border:1px solid var(--border); display:grid; gap:.35rem; min-height:8rem; padding:.75rem; } .bento-card h2 { color:var(--muted); font-size:.7rem; letter-spacing:.07em; text-transform:uppercase; } [data-next-actions] { background:var(--surface); border:1px solid var(--border); margin-top:.65rem; padding:.75rem; } [data-next-actions] h2 { margin-bottom:.65rem; } .project-chrome { display:flex; flex-wrap:wrap; gap:.5rem 1rem; justify-content:space-between; } [data-project-nav] { display:flex; gap:.65rem; font-size:.72rem; } [data-project-nav] [aria-current] { color:var(--cyan); font-weight:700; }
43
+ .evidence-grid.compact-composition { display:grid; gap:.65rem; grid-template-columns:repeat(2,minmax(0,1fr)); padding:.65rem; } .evidence-grid h2 { font-size:.9rem; margin-bottom:.4rem; } .plan-card { border:1px solid var(--border); display:grid; gap:.35rem; margin-top:.4rem; padding:.55rem; } .plan-card.active { border-left:3px solid var(--indigo); } .plan-card.blocked { border-left:3px solid var(--red); } .plan-card > div { align-items:center; display:flex; justify-content:space-between; gap:.5rem; } .plan-card p { color:var(--muted); font-size:.72rem; margin:0; }
44
+ .evidence-table { margin-top:.65rem; } .closure-actions { align-items:center; border-top:1px solid var(--border); display:flex; gap:1rem; justify-content:space-between; margin-top:1rem; padding:1rem; } .closure-actions h3 { font-size:.9rem; margin:0 0:.25rem; }
45
+ .action-list li { align-items:center; border-bottom:1px solid var(--border); display:grid; gap:.6rem; grid-template-columns:max-content minmax(8rem,1fr) minmax(10rem,auto) max-content; padding:.65rem 0; } .action-list li:last-child { border-bottom:0; }
46
+ .sr-only { clip:rect(0 0 0 0); clip-path:inset(50%); height:1px; overflow:hidden; position:absolute; white-space:nowrap; width:1px; }
47
+ a:focus-visible { outline:3px solid var(--cyan); outline-offset:3px; }
48
+ @media (max-width: 720px) { .shell { display:block; } nav { border-bottom:1px solid var(--border); border-right:0; } nav ul { grid-template-columns:repeat(2,minmax(0,1fr)); } main { padding:0 1rem 2rem; } .dashboard-toolbar { margin:0 -1rem; padding:.65rem 1rem; } .dashboard-toolbar form { margin-left:0; order:3; width:100%; } .dashboard-toolbar input { width:100%; } .privacy-body,.closure-actions { align-items:stretch; flex-direction:column; } .diagnostic-grid,.timeline,.evidence-grid,.machine-bento { grid-template-columns:1fr; } .action-list li { grid-template-columns:max-content 1fr; } .action-list code,.action-list button { grid-column:1 / -1; } table,thead,tbody,tr,th,td { display:block; } thead { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); } td { border:0; padding:.25rem 0; } tr { border-bottom:1px solid var(--border); padding:.7rem 0; } tr:last-child { border-bottom:0; } td::before { color:var(--muted); content:attr(data-label) ": "; font-size:.72rem; text-transform:uppercase; } }
49
+ @media (prefers-reduced-motion: reduce) { *,*::before,*::after { animation-duration:.01ms !important; animation-iteration-count:1 !important; scroll-behavior:auto !important; transition-duration:.01ms !important; } }
50
+ @media print { :root { color-scheme:light; } body { background:#fff; color:#000; } .shell { display:block; } nav { display:none; } main { max-width:none; padding:0; } section { break-inside:avoid; border-color:#666; } section > header { background:#eee; } .state,.availability,code { color:#000 !important; } }
51
+ `;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dashboardSnapshot = dashboardSnapshot;
4
+ const PROJECT_SECTION_IDS = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
5
+ /** Safe, deterministic V1 fixture and renderer input. */
6
+ function dashboardSnapshot(overrides = {}) {
7
+ const { project: projectOverride, sections: sectionsOverride, ...rest } = overrides;
8
+ const project = projectOverride ?? { detected: false, label: 'No project detected' };
9
+ const sections = sectionsOverride ?? (project.detected
10
+ ? PROJECT_SECTION_IDS.map((id) => ({ id, availability: id === 'machine' ? 'available' : 'not_applicable', items: [] }))
11
+ : [{ id: 'machine', availability: 'available', items: [] }]);
12
+ return {
13
+ schema: 1,
14
+ generatedAt: '2026-08-22T00:00:00.000Z',
15
+ overall: 'healthy',
16
+ project,
17
+ confidence: 'none',
18
+ sections,
19
+ ...rest,
20
+ };
21
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DashboardValidationError = void 0;
4
+ exports.validateDashboardSnapshotV1 = validateDashboardSnapshotV1;
5
+ const SECTION_ORDER = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
6
+ const ITEM_STATES = ['ok', 'attention', 'missing', 'unavailable', 'not_applicable'];
7
+ const AVAILABILITIES = ['available', 'unavailable', 'not_applicable'];
8
+ const OVERALLS = ['healthy', 'degraded'];
9
+ const CONFIDENCES = ['none', 'provisional', 'observing', 'supported'];
10
+ const ACTIONABLE_STATES = new Set(['attention', 'missing', 'unavailable']);
11
+ class DashboardValidationError extends Error {
12
+ constructor(message) {
13
+ super(`Invalid DashboardSnapshotV1: ${message}`);
14
+ this.name = 'DashboardValidationError';
15
+ }
16
+ }
17
+ exports.DashboardValidationError = DashboardValidationError;
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ }
21
+ function assertRecord(value, path) {
22
+ if (!isRecord(value))
23
+ throw new DashboardValidationError(`${path} must be an object`);
24
+ }
25
+ function assertOnlyKeys(value, keys, path) {
26
+ for (const key of Object.keys(value)) {
27
+ if (!keys.includes(key))
28
+ throw new DashboardValidationError(`${path}.${key} is not supported`);
29
+ }
30
+ }
31
+ function assertNonEmptyString(value, path) {
32
+ if (typeof value !== 'string' || value.trim() === '') {
33
+ throw new DashboardValidationError(`${path} must be a non-empty string`);
34
+ }
35
+ }
36
+ function assertEnum(value, values, path) {
37
+ if (typeof value !== 'string' || !values.includes(value)) {
38
+ throw new DashboardValidationError(`${path} must be one of ${values.join(', ')}`);
39
+ }
40
+ }
41
+ /** Validates the durable public dashboard boundary without coercing unknown input. */
42
+ function validateDashboardSnapshotV1(value) {
43
+ assertRecord(value, 'snapshot');
44
+ assertOnlyKeys(value, ['schema', 'generatedAt', 'overall', 'project', 'confidence', 'sections'], 'snapshot');
45
+ if (value.schema !== 1)
46
+ throw new DashboardValidationError('schema must be version 1');
47
+ assertNonEmptyString(value.generatedAt, 'generatedAt');
48
+ if (Number.isNaN(Date.parse(value.generatedAt)))
49
+ throw new DashboardValidationError('generatedAt must be a valid date-time');
50
+ assertEnum(value.overall, OVERALLS, 'overall');
51
+ assertEnum(value.confidence, CONFIDENCES, 'confidence');
52
+ assertRecord(value.project, 'project');
53
+ assertOnlyKeys(value.project, ['detected', 'label'], 'project');
54
+ if (typeof value.project.detected !== 'boolean')
55
+ throw new DashboardValidationError('project.detected must be boolean');
56
+ assertNonEmptyString(value.project.label, 'project.label');
57
+ if (!Array.isArray(value.sections))
58
+ throw new DashboardValidationError('sections must be an array');
59
+ const sectionIds = new Set();
60
+ const itemIds = new Set();
61
+ let previousOrder = -1;
62
+ for (const [sectionIndex, section] of value.sections.entries()) {
63
+ const sectionPath = `sections[${sectionIndex}]`;
64
+ assertRecord(section, sectionPath);
65
+ assertOnlyKeys(section, ['id', 'availability', 'items'], sectionPath);
66
+ assertEnum(section.id, SECTION_ORDER, `${sectionPath}.id`);
67
+ const order = SECTION_ORDER.indexOf(section.id);
68
+ if (order <= previousOrder)
69
+ throw new DashboardValidationError('section order must be deterministic');
70
+ previousOrder = order;
71
+ if (sectionIds.has(section.id))
72
+ throw new DashboardValidationError(`duplicate section id ${section.id}`);
73
+ sectionIds.add(section.id);
74
+ assertEnum(section.availability, AVAILABILITIES, `${sectionPath}.availability`);
75
+ if (!Array.isArray(section.items))
76
+ throw new DashboardValidationError(`${sectionPath}.items must be an array`);
77
+ for (const [itemIndex, item] of section.items.entries()) {
78
+ const itemPath = `${sectionPath}.items[${itemIndex}]`;
79
+ assertRecord(item, itemPath);
80
+ assertOnlyKeys(item, ['id', 'label', 'state', 'detail', 'remediation'], itemPath);
81
+ assertNonEmptyString(item.id, `${itemPath}.id`);
82
+ if (itemIds.has(item.id))
83
+ throw new DashboardValidationError(`duplicate item id ${item.id}`);
84
+ itemIds.add(item.id);
85
+ assertNonEmptyString(item.label, `${itemPath}.label`);
86
+ assertEnum(item.state, ITEM_STATES, `${itemPath}.state`);
87
+ if (item.detail !== undefined)
88
+ assertNonEmptyString(item.detail, `${itemPath}.detail`);
89
+ if (item.remediation !== undefined)
90
+ assertNonEmptyString(item.remediation, `${itemPath}.remediation`);
91
+ if (ACTIONABLE_STATES.has(item.state) && item.remediation === undefined) {
92
+ throw new DashboardValidationError(`${itemPath}.remediation is required for actionable state`);
93
+ }
94
+ if (item.state === 'not_applicable' && item.remediation !== undefined) {
95
+ throw new DashboardValidationError(`${itemPath}.remediation is forbidden for not_applicable state`);
96
+ }
97
+ }
98
+ }
99
+ if (!value.project.detected && (value.sections.length !== 1 || value.sections[0].id !== 'machine')) {
100
+ throw new DashboardValidationError('no project snapshot may contain only the machine section');
101
+ }
102
+ if (value.project.detected && sectionIds.size !== SECTION_ORDER.length) {
103
+ throw new DashboardValidationError('detected project snapshot must contain the canonical lifecycle sections');
104
+ }
105
+ return value;
106
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveHtmlTarget = resolveHtmlTarget;
7
+ exports.writeHtmlAtomically = writeHtmlAtomically;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const crypto_1 = require("crypto");
11
+ function resolveHtmlTarget(input, operations = fs_1.default) {
12
+ if (!input || typeof input.cwd !== 'string' || input.cwd.trim() === '' || input.cwd.includes('\0') || typeof input.target !== 'string' || input.target.trim() === '' || input.target.includes('\0') || input.target.startsWith('--'))
13
+ throw new Error('--html requires a file target');
14
+ if (input.force !== undefined && typeof input.force !== 'boolean')
15
+ throw new Error('--force must be boolean');
16
+ const target = path_1.default.isAbsolute(input.target) ? path_1.default.normalize(input.target) : path_1.default.resolve(input.cwd, input.target);
17
+ const parent = path_1.default.dirname(target);
18
+ if (!operations.existsSync(parent) || !operations.statSync(parent).isDirectory())
19
+ throw new Error(`HTML parent directory does not exist: ${parent}`);
20
+ for (let current = parent;; current = path_1.default.dirname(current)) {
21
+ if (operations.lstatSync(current).isSymbolicLink())
22
+ throw new Error('HTML parent directory must not contain a symbolic link');
23
+ if (current === path_1.default.parse(current).root)
24
+ break;
25
+ }
26
+ try {
27
+ operations.accessSync(parent, fs_1.default.constants.W_OK);
28
+ }
29
+ catch {
30
+ throw new Error(`HTML parent directory is not writable: ${parent}`);
31
+ }
32
+ let stat;
33
+ try {
34
+ stat = operations.lstatSync(target);
35
+ }
36
+ catch (error) {
37
+ if (error.code === 'ENOENT')
38
+ return target;
39
+ throw error;
40
+ }
41
+ if (stat.isSymbolicLink() || !stat.isFile())
42
+ throw new Error('HTML target must be a regular file');
43
+ if (!input.force)
44
+ throw new Error(`HTML target already exists: ${target}; use --force`);
45
+ return target;
46
+ }
47
+ function writeHtmlAtomically(input, operations = fs_1.default) {
48
+ if (!input || typeof input.cwd !== 'string' || input.cwd.trim() === '' || input.cwd.includes('\0'))
49
+ throw new Error('writeHtmlAtomically requires a non-empty cwd');
50
+ if (typeof input.target !== 'string' || input.target.trim() === '' || !path_1.default.isAbsolute(input.target))
51
+ throw new Error('writeHtmlAtomically requires a non-empty absolute target');
52
+ if (input.target.includes('\0'))
53
+ throw new Error('writeHtmlAtomically target must not contain NUL');
54
+ if (typeof input.html !== 'string' || input.html.length === 0)
55
+ throw new Error('writeHtmlAtomically requires non-empty html');
56
+ if (input.force !== undefined && typeof input.force !== 'boolean')
57
+ throw new Error('writeHtmlAtomically force must be boolean');
58
+ if (operations.existsSync(input.target) && !input.force)
59
+ throw new Error(`HTML target already exists: ${input.target}; use --force`);
60
+ const platform = input.platform ?? process.platform;
61
+ if (typeof platform !== 'string' || !/^[a-z0-9]+$/i.test(platform))
62
+ throw new Error('writeHtmlAtomically platform must be a valid platform');
63
+ const temp = path_1.default.join(path_1.default.dirname(input.target), `.${path_1.default.basename(input.target)}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`);
64
+ let fd;
65
+ let tempCreated = false;
66
+ try {
67
+ fd = platform === 'win32' ? operations.openSync(temp, 'wx') : operations.openSync(temp, 'wx', 0o600);
68
+ tempCreated = true;
69
+ operations.writeFileSync(fd, input.html, 'utf8');
70
+ operations.fsyncSync(fd);
71
+ operations.closeSync(fd);
72
+ fd = undefined;
73
+ operations.renameSync(temp, input.target);
74
+ }
75
+ catch (error) {
76
+ if (fd !== undefined)
77
+ try {
78
+ operations.closeSync(fd);
79
+ }
80
+ catch { /* best effort */ }
81
+ try {
82
+ if (tempCreated && operations.existsSync(temp))
83
+ operations.unlinkSync(temp);
84
+ }
85
+ catch { /* known temp only */ }
86
+ throw error;
87
+ }
88
+ }
@@ -297,7 +297,7 @@ function gatherProject(root, bundles, agent = 'claude-code') {
297
297
  context = { present: true, file: 'AGENTS.md' };
298
298
  return {
299
299
  root,
300
- profile: { present: profilePresent, extensions: profile.extensions },
300
+ profile: { present: profilePresent, extensions: profile.extensions, registries: profile.registries },
301
301
  activeBundles: { expected, linked, broken },
302
302
  orphanLinks,
303
303
  sensors: { present: fs_1.default.existsSync(path_1.default.join(root, '.awm', 'sensors.json')) },