agentic-workflow-manager 8.3.0 → 8.5.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 (36) hide show
  1. package/dist/src/commands/doctor.js +40 -1
  2. package/dist/src/commands/evidence/index.js +95 -0
  3. package/dist/src/core/dashboard/collect.js +223 -0
  4. package/dist/src/core/dashboard/plan-state.js +44 -0
  5. package/dist/src/core/dashboard/render-html.js +88 -0
  6. package/dist/src/core/dashboard/render-terminal.js +45 -0
  7. package/dist/src/core/dashboard/sanitize.js +62 -0
  8. package/dist/src/core/dashboard/styles.js +51 -0
  9. package/dist/src/core/dashboard/types.js +21 -0
  10. package/dist/src/core/dashboard/validate.js +106 -0
  11. package/dist/src/core/dashboard/write-html.js +88 -0
  12. package/dist/src/core/diagnostics/context.js +1 -1
  13. package/dist/src/core/evidence/capture.js +46 -0
  14. package/dist/src/core/evidence/history.js +50 -0
  15. package/dist/src/core/evidence/store.js +32 -0
  16. package/dist/src/core/evidence/types.js +97 -0
  17. package/dist/src/index.js +2 -0
  18. package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
  19. package/dist/tests/commands/doctor.test.js +160 -0
  20. package/dist/tests/core/dashboard/collect.test.js +242 -0
  21. package/dist/tests/core/dashboard/contracts.test.js +92 -0
  22. package/dist/tests/core/dashboard/plan-state.test.js +32 -0
  23. package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
  24. package/dist/tests/core/dashboard/render-html.test.js +191 -0
  25. package/dist/tests/core/dashboard/render-terminal.test.js +72 -0
  26. package/dist/tests/core/dashboard/write-html.test.js +112 -0
  27. package/dist/tests/core/evidence/capture.test.js +48 -0
  28. package/dist/tests/core/evidence/command.test.js +37 -0
  29. package/dist/tests/core/evidence/history.test.js +41 -0
  30. package/dist/tests/core/evidence/store.test.js +32 -0
  31. package/dist/tests/core/evidence/types.test.js +30 -0
  32. package/dist/tests/helpers/dashboard-fixtures.js +66 -0
  33. package/dist/tests/helpers/evidence-fixtures.js +17 -0
  34. package/dist/tests/integration/doctor-dashboard.e2e.test.js +177 -0
  35. package/dist/tests/integration/published-doctor-evidence.e2e.test.js +180 -0
  36. package/package.json +1 -1
@@ -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')) },
@@ -0,0 +1,46 @@
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.captureCycleEvidence = captureCycleEvidence;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const types_1 = require("./types");
9
+ const hash = (value) => crypto_1.default.createHash('sha256').update(value).digest('hex');
10
+ const object = (value, name) => { if (!value || typeof value !== 'object' || Array.isArray(value))
11
+ throw new Error(`${name} must be an object`); return value; };
12
+ const iso = (value, name) => { if (typeof value !== 'string' || Number.isNaN(Date.parse(value)))
13
+ throw new Error(`${name} must be an ISO timestamp`); return value; };
14
+ const nonNegative = (value, name) => { if (!Number.isSafeInteger(value) || value < 0)
15
+ throw new Error(`${name} must be a non-negative integer`); return value; };
16
+ function captureCycleEvidence(input) {
17
+ if (!input || typeof input.root !== 'string' || !input.root)
18
+ throw new Error('capture root is required');
19
+ if (typeof input.repositoryIdentity !== 'string' || input.repositoryIdentity.length === 0 || input.repositoryIdentity.length > 4096 || /[\r\n]/.test(input.repositoryIdentity))
20
+ throw new Error('repository identity is invalid');
21
+ const repositoryIdentity = hash(input.repositoryIdentity);
22
+ if (typeof input.planPath !== 'string' || !input.planPath)
23
+ throw new Error('capture planPath is required');
24
+ const journal = object(input.journal, 'journal');
25
+ const cycle = object(journal.cycle, 'journal cycle');
26
+ const startedAt = iso(cycle.startedAt, 'cycle startedAt');
27
+ const endedAt = iso(cycle.completedAt ?? journal.controllerHeartbeatAt, 'cycle endedAt');
28
+ if (cycle.status !== 'COMPLETE' && cycle.status !== 'BLOCKED')
29
+ throw new Error('journal cycle must be complete or blocked');
30
+ if (!Array.isArray(journal.tasks) || !Array.isArray(journal.verdicts) || !Array.isArray(journal.fixes) || !Array.isArray(input.gates) || !Array.isArray(input.ledger))
31
+ throw new Error('capture sources are invalid');
32
+ const tasks = journal.tasks.map((item, index) => { const task = object(item, `task ${index}`); if (typeof task.id !== 'string')
33
+ throw new Error('task id is invalid'); const attempts = nonNegative(task.attempts, 'task attempts'); return { id: task.id, attempts, retries: Math.max(attempts - 1, 0) }; });
34
+ const failures = journal.verdicts.filter((item, index) => { const verdict = object(item, `verdict ${index}`); if (verdict.result !== 'pass' && verdict.result !== 'fail' && verdict.result !== 'inconclusive')
35
+ throw new Error('verdict result is invalid'); iso(verdict.receivedAt, 'verdict receivedAt'); return verdict.result === 'fail'; });
36
+ const signatures = failures.map((item) => { const verdict = object(item, 'verdict'); if (typeof verdict.fingerprint !== 'string' || !verdict.fingerprint)
37
+ throw new Error('verdict fingerprint is required'); return hash(`signature:${verdict.fingerprint}`); });
38
+ const fixes = journal.fixes.filter((item, index) => { const fix = object(item, `fix ${index}`); if (typeof fix.closed !== 'boolean')
39
+ throw new Error('fix closed is invalid'); return fix.closed; }).length;
40
+ const gates = input.gates.map((item, index) => { const gate = object(item, `gate ${index}`); if (typeof gate.required !== 'boolean' || typeof gate.passed !== 'boolean')
41
+ throw new Error('gate is invalid'); return gate; }).filter((gate) => gate.required === true);
42
+ const cures = input.ledger.filter((item, index) => { const entry = object(item, `ledger ${index}`); if (typeof entry.signature !== 'string' || (entry.polarity !== 'finding' && entry.polarity !== 'win'))
43
+ throw new Error('ledger entry is invalid'); iso(entry.ts, 'ledger timestamp'); return entry.polarity === 'win'; }).map((entry) => { const source = object(entry, 'ledger'); return { signature: hash(`signature:${source.signature}`), curedAt: iso(source.ts, 'ledger timestamp') }; });
44
+ const pr = input.pr === undefined ? undefined : (() => { const raw = object(input.pr, 'pr'); return { provider: raw.provider, number: raw.number }; })();
45
+ return (0, types_1.validateCycleEvidence)({ schema: 1, cycleId: hash(`${repositoryIdentity}\0${input.planPath}\0${startedAt}`), startedAt, endedAt, durationMs: Date.parse(endedAt) - Date.parse(startedAt), cycleState: cycle.status === 'COMPLETE' ? 'completed' : 'blocked', plan: { ref: input.planPath, state: input.planState ?? (cycle.status === 'BLOCKED' ? 'blocked' : 'executed') }, tasks, qa: { findings: signatures.length, fixes: Math.min(fixes, signatures.length), signatures }, gates: { required: gates.length, firstEvaluationsPassed: gates.map((gate) => gate.passed), firstPass: gates.every((gate) => gate.passed === true) }, cures, ...(pr ? { pr } : {}) });
46
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.confidenceForCycles = confidenceForCycles;
4
+ exports.classifyCure = classifyCure;
5
+ exports.buildEvidenceHistory = buildEvidenceHistory;
6
+ const types_1 = require("./types");
7
+ function confidenceForCycles(count) {
8
+ if (!Number.isSafeInteger(count) || count < 0)
9
+ throw new Error('eligible cycle count must be a non-negative integer');
10
+ const eligibleCycles = count;
11
+ if (eligibleCycles === 0)
12
+ return 'none';
13
+ if (eligibleCycles === 1)
14
+ return 'provisional';
15
+ if (eligibleCycles < 5)
16
+ return 'observing';
17
+ return 'supported';
18
+ }
19
+ function classifyCure(input) {
20
+ if (!input || typeof input !== 'object' || Array.isArray(input))
21
+ throw new Error('cure observation must be an object');
22
+ const value = input;
23
+ if (Object.keys(value).some((key) => key !== 'laterEligibleCycles' && key !== 'recurred') || !Number.isSafeInteger(value.laterEligibleCycles) || value.laterEligibleCycles < 0 || typeof value.recurred !== 'boolean')
24
+ throw new Error('cure observation is invalid');
25
+ const laterEligibleCycles = value.laterEligibleCycles;
26
+ if (value.recurred)
27
+ return 'recurred';
28
+ if (laterEligibleCycles === 0)
29
+ return 'awaiting_observation';
30
+ return laterEligibleCycles >= 3 ? 'supported' : 'observing';
31
+ }
32
+ /** Validates, retains, and deterministically orders every eligible local observation. */
33
+ function buildEvidenceHistory(records) {
34
+ if (!Array.isArray(records))
35
+ throw new Error('evidence history records must be an array');
36
+ const valid = records.map((record) => (0, types_1.validateCycleEvidence)(record)).sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.cycleId.localeCompare(right.cycleId));
37
+ const completed = valid.filter((cycle) => cycle.cycleState === 'completed');
38
+ const cycles = valid.map((cycle, index) => ({
39
+ ...cycle,
40
+ retries: cycle.tasks.reduce((total, task) => total + task.retries, 0),
41
+ cureEfficacy: cycle.cures.map((cure) => {
42
+ const later = valid.slice(index + 1).filter((candidate) => candidate.cycleState === 'completed');
43
+ return {
44
+ signature: cure.signature,
45
+ efficacy: classifyCure({ laterEligibleCycles: later.length, recurred: later.some((candidate) => candidate.qa.signatures.includes(cure.signature)) }),
46
+ };
47
+ }),
48
+ }));
49
+ return { confidence: confidenceForCycles(completed.length), empty: cycles.length === 0, cycles };
50
+ }
@@ -0,0 +1,32 @@
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.writeCycleEvidence = writeCycleEvidence;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const atomic_file_1 = require("../atomic-file");
10
+ const types_1 = require("./types");
11
+ function writeCycleEvidence(root, evidence) {
12
+ if (typeof root !== 'string' || root.length === 0)
13
+ throw new Error('evidence root must be a non-empty string');
14
+ const valid = (0, types_1.validateCycleEvidence)(evidence);
15
+ const safeRoot = safeDirectory(root, 'root');
16
+ let directory = safeRoot;
17
+ for (const segment of ['.awm', 'evidence', 'cycles']) {
18
+ directory = path_1.default.join(directory, segment);
19
+ if (!fs_1.default.existsSync(directory))
20
+ fs_1.default.mkdirSync(directory, { mode: 0o700 });
21
+ directory = safeDirectory(directory, `evidence ${segment}`);
22
+ }
23
+ const file = path_1.default.join(directory, `${valid.cycleId}.json`);
24
+ (0, atomic_file_1.writeFileAtomicDurable)(file, JSON.stringify(valid, null, 2) + '\n', 0o600);
25
+ return valid;
26
+ }
27
+ function safeDirectory(directory, label) {
28
+ const stat = fs_1.default.lstatSync(directory);
29
+ if (stat.isSymbolicLink() || !stat.isDirectory())
30
+ throw new Error(`unsafe evidence ${label} directory symlink or non-directory`);
31
+ return fs_1.default.realpathSync(directory);
32
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLAN_STATES = void 0;
4
+ exports.validateCycleEvidence = validateCycleEvidence;
5
+ exports.PLAN_STATES = ['active', 'blocked', 'qa_pending', 'retro_pending', 'executed', 'legacy_unverifiable'];
6
+ const DIGEST = /^[a-f0-9]{64}$/;
7
+ const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
8
+ function record(value, label) {
9
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
10
+ throw new Error(`${label} must be an object`);
11
+ return value;
12
+ }
13
+ function keys(value, label, allowed) {
14
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
15
+ throw new Error(`${label} has unsupported fields`);
16
+ }
17
+ function timestamp(value, label) {
18
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value)
19
+ throw new Error(`${label} must be a canonical ISO timestamp`);
20
+ return value;
21
+ }
22
+ function count(value, label) {
23
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
24
+ throw new Error(`${label} must be a non-negative integer`);
25
+ return value;
26
+ }
27
+ function digest(value, label) {
28
+ if (typeof value !== 'string' || !DIGEST.test(value))
29
+ throw new Error(`${label} must be a SHA-256 digest`);
30
+ return value;
31
+ }
32
+ function relativePlanRef(value) {
33
+ if (typeof value !== 'string' || !value || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value)
34
+ || value.split('/').some((part) => part === '' || part === '.' || part === '..'))
35
+ throw new Error('plan.ref must be repo-relative');
36
+ return value;
37
+ }
38
+ /** Strict public durable-boundary validator: accepted evidence intentionally has no prose or identities. */
39
+ function validateCycleEvidence(input) {
40
+ const value = record(input, 'cycle evidence');
41
+ keys(value, 'cycle evidence', ['schema', 'cycleId', 'startedAt', 'endedAt', 'durationMs', 'cycleState', 'plan', 'tasks', 'qa', 'gates', 'cures', 'pr']);
42
+ if (value.schema !== 1)
43
+ throw new Error('cycle evidence schema must be 1');
44
+ const startedAt = timestamp(value.startedAt, 'startedAt');
45
+ const endedAt = timestamp(value.endedAt, 'endedAt');
46
+ const durationMs = count(value.durationMs, 'durationMs');
47
+ if (Date.parse(endedAt) < Date.parse(startedAt) || durationMs !== Date.parse(endedAt) - Date.parse(startedAt))
48
+ throw new Error('cycle duration is invalid');
49
+ if (value.cycleState !== 'completed' && value.cycleState !== 'blocked')
50
+ throw new Error('cycleState is invalid');
51
+ const plan = record(value.plan, 'plan');
52
+ keys(plan, 'plan', ['ref', 'state']);
53
+ if (typeof plan.state !== 'string' || !exports.PLAN_STATES.includes(plan.state))
54
+ throw new Error('plan state is invalid');
55
+ if (!Array.isArray(value.tasks))
56
+ throw new Error('tasks must be an array');
57
+ const tasks = value.tasks.map((item, index) => {
58
+ const task = record(item, `tasks[${index}]`);
59
+ keys(task, `tasks[${index}]`, ['id', 'attempts', 'retries']);
60
+ if (typeof task.id !== 'string' || !SAFE_ID.test(task.id))
61
+ throw new Error('task id is invalid');
62
+ const attempts = count(task.attempts, 'task attempts');
63
+ const retries = count(task.retries, 'task retries');
64
+ if (retries !== Math.max(attempts - 1, 0))
65
+ throw new Error('task retries must derive from attempts');
66
+ return { id: task.id, attempts, retries };
67
+ });
68
+ const qa = record(value.qa, 'qa');
69
+ keys(qa, 'qa', ['findings', 'fixes', 'signatures']);
70
+ if (!Array.isArray(qa.signatures))
71
+ throw new Error('qa signatures must be an array');
72
+ const signatures = qa.signatures.map((signature, index) => digest(signature, `qa.signatures[${index}]`));
73
+ const findings = count(qa.findings, 'qa findings');
74
+ const fixes = count(qa.fixes, 'qa fixes');
75
+ if (signatures.length !== findings || fixes > findings)
76
+ throw new Error('qa counts are inconsistent');
77
+ const gates = record(value.gates, 'gates');
78
+ keys(gates, 'gates', ['required', 'firstEvaluationsPassed', 'firstPass']);
79
+ const required = count(gates.required, 'required gates');
80
+ if (!Array.isArray(gates.firstEvaluationsPassed) || !gates.firstEvaluationsPassed.every((passed) => typeof passed === 'boolean') || gates.firstEvaluationsPassed.length !== required || typeof gates.firstPass !== 'boolean' || gates.firstPass !== gates.firstEvaluationsPassed.every(Boolean))
81
+ throw new Error('gate evaluations are inconsistent');
82
+ if (!Array.isArray(value.cures))
83
+ throw new Error('cures must be an array');
84
+ const cures = value.cures.map((item, index) => { const cure = record(item, `cures[${index}]`); keys(cure, `cures[${index}]`, ['signature', 'curedAt']); return { signature: digest(cure.signature, 'cure signature'), curedAt: timestamp(cure.curedAt, 'curedAt') }; });
85
+ let pr;
86
+ if (value.pr !== undefined) {
87
+ const raw = record(value.pr, 'pr');
88
+ keys(raw, 'pr', ['provider', 'number']);
89
+ if (raw.provider !== 'github' && raw.provider !== 'gitlab' && raw.provider !== 'other')
90
+ throw new Error('pr provider is invalid');
91
+ const number = count(raw.number, 'pr number');
92
+ if (number < 1)
93
+ throw new Error('pr number is invalid');
94
+ pr = { provider: raw.provider, number };
95
+ }
96
+ return { schema: 1, cycleId: digest(value.cycleId, 'cycleId'), startedAt, endedAt, durationMs, cycleState: value.cycleState, plan: { ref: relativePlanRef(plan.ref), state: plan.state }, tasks, qa: { findings, fixes, signatures }, gates: { required, firstEvaluationsPassed: [...gates.firstEvaluationsPassed], firstPass: gates.firstPass }, cures, ...(pr ? { pr } : {}) };
97
+ }
package/dist/src/index.js CHANGED
@@ -41,6 +41,7 @@ const agent_1 = require("./commands/agent");
41
41
  const job_1 = require("./commands/job");
42
42
  const watch_1 = require("./commands/watch");
43
43
  const track_1 = require("./commands/track");
44
+ const evidence_1 = require("./commands/evidence");
44
45
  const add_1 = require("./commands/add");
45
46
  const sync_1 = require("./commands/sync");
46
47
  const update_1 = require("./commands/update");
@@ -733,6 +734,7 @@ miroCmd.command('sync <storyMapPath>')
733
734
  (0, job_1.registerJobCommand)(program);
734
735
  (0, watch_1.registerWatchCommand)(program);
735
736
  (0, track_1.registerTrackCommand)(program);
737
+ (0, evidence_1.registerEvidenceCommand)(program);
736
738
  // Commander only waits for async action handlers through parseAsync(). The CLI has
737
739
  // async commands (including `sensors coverage`), so returning its promise keeps the
738
740
  // process alive until their JSON/output contract has been completed.