@shawnowen/comet-mcp 2.3.1 → 2.4.1

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 (85) hide show
  1. package/README.md +86 -19
  2. package/dist/alert-dispatcher.d.ts +23 -0
  3. package/dist/alert-dispatcher.js +101 -0
  4. package/dist/bound-session.d.ts +23 -0
  5. package/dist/bound-session.js +119 -0
  6. package/dist/bridge-config.d.ts +6 -0
  7. package/dist/bridge-config.js +78 -0
  8. package/dist/cdp-client.d.ts +40 -4
  9. package/dist/cdp-client.js +502 -155
  10. package/dist/comet-ai.d.ts +15 -0
  11. package/dist/comet-ai.js +114 -38
  12. package/dist/delegate-binding.d.ts +19 -0
  13. package/dist/delegate-binding.js +73 -0
  14. package/dist/discovery/capability-entry.d.ts +215 -0
  15. package/dist/discovery/capability-entry.js +13 -0
  16. package/dist/discovery/description-template.d.ts +40 -0
  17. package/dist/discovery/description-template.js +61 -0
  18. package/dist/discovery/golden-queries.fixture.d.ts +22 -0
  19. package/dist/discovery/golden-queries.fixture.js +137 -0
  20. package/dist/discovery/mcp-source.d.ts +38 -0
  21. package/dist/discovery/mcp-source.js +70 -0
  22. package/dist/discovery/metadata-completeness.d.ts +48 -0
  23. package/dist/discovery/metadata-completeness.js +83 -0
  24. package/dist/discovery/registry.d.ts +35 -0
  25. package/dist/discovery/registry.js +35 -0
  26. package/dist/discovery/safety.d.ts +44 -0
  27. package/dist/discovery/safety.js +59 -0
  28. package/dist/discovery/schema-validator.d.ts +36 -0
  29. package/dist/discovery/schema-validator.js +257 -0
  30. package/dist/discovery/source-error.d.ts +47 -0
  31. package/dist/discovery/source-error.js +95 -0
  32. package/dist/discovery/tool-meta.d.ts +41 -0
  33. package/dist/discovery/tool-meta.js +229 -0
  34. package/dist/discovery/virtual-tools.d.ts +20 -0
  35. package/dist/discovery/virtual-tools.js +69 -0
  36. package/dist/http-server.js +2067 -47
  37. package/dist/index.js +3163 -710
  38. package/dist/observer.d.ts +47 -0
  39. package/dist/observer.js +516 -0
  40. package/dist/session-registry.d.ts +57 -0
  41. package/dist/session-registry.js +500 -0
  42. package/dist/sidecar-artifacts.d.ts +49 -0
  43. package/dist/sidecar-artifacts.js +146 -0
  44. package/dist/snapshot-capture.d.ts +3 -0
  45. package/dist/snapshot-capture.js +91 -0
  46. package/dist/tab-group-archive.js +3 -1
  47. package/dist/tab-groups.d.ts +7 -0
  48. package/dist/tab-groups.js +21 -3
  49. package/dist/task-thread-aggregator.d.ts +34 -0
  50. package/dist/task-thread-aggregator.js +480 -0
  51. package/dist/task-thread-canonical.d.ts +142 -0
  52. package/dist/task-thread-canonical.js +116 -0
  53. package/dist/types.d.ts +237 -0
  54. package/dist/window-bindings.d.ts +112 -0
  55. package/dist/window-bindings.js +476 -0
  56. package/extension/background.js +1556 -300
  57. package/extension/icons/icon.svg +9 -0
  58. package/extension/icons/icon128.png +0 -0
  59. package/extension/icons/icon16.png +0 -0
  60. package/extension/icons/icon48.png +0 -0
  61. package/extension/manifest.json +19 -4
  62. package/extension/session-logic.js +2383 -0
  63. package/extension/session-manager.html +299 -0
  64. package/extension/sidepanel.css +5323 -528
  65. package/extension/sidepanel.html +282 -2
  66. package/extension/sidepanel.js +10075 -951
  67. package/extension/window-policy.js +162 -0
  68. package/package.json +10 -7
  69. package/vendor/lifecycle-mcp-adapter.mjs +103 -0
  70. package/vendor/lifecycle-metadata.mjs +252 -0
  71. package/vendor/readiness-report.mjs +742 -0
  72. package/dist/cdp-client.d.ts.map +0 -1
  73. package/dist/cdp-client.js.map +0 -1
  74. package/dist/comet-ai.d.ts.map +0 -1
  75. package/dist/comet-ai.js.map +0 -1
  76. package/dist/http-server.d.ts.map +0 -1
  77. package/dist/http-server.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/tab-group-archive.d.ts.map +0 -1
  81. package/dist/tab-group-archive.js.map +0 -1
  82. package/dist/tab-groups.d.ts.map +0 -1
  83. package/dist/tab-groups.js.map +0 -1
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,742 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Readiness Reporter — machine-readable + human-readable readiness reports (Spec 070, S070-T007).
4
+ * Operational Telemetry — route/fallback/ownership/completion aggregation (Spec 070, S070-T019).
5
+ *
6
+ * Runs bootstrap checks via comet-bootstrap.mjs and annotates results with
7
+ * two-tier readiness labels (Pilot / Broad) per clarification C3.
8
+ *
9
+ * Can be used as:
10
+ * 1. CLI: node scripts/readiness-report.mjs [--json] [--save <path>] [--port <n>]
11
+ * 2. API: import { generateReadinessReport, recordTelemetryEvent } from './readiness-report.mjs';
12
+ *
13
+ * Readiness tiers (C3):
14
+ * - Pilot: core + CDP + config + output dirs pass; MCP and extension may warn.
15
+ * - Broad: all checks pass including MCP build and extension bridge.
16
+ *
17
+ * Telemetry (S070-T019):
18
+ * Events are appended to {COMET_OUTPUT_BASE}/audit/telemetry-events.jsonl via
19
+ * recordTelemetryEvent(). The report aggregates recent events into route usage,
20
+ * fallback frequency, ownership check results, and completion outcome stats.
21
+ *
22
+ * Exit codes mirror bootstrap: 0 = Broad-ready, 1 = critical failure, 2 = Pilot-only (warnings).
23
+ */
24
+
25
+ import { spawnSync } from "child_process";
26
+ import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from "fs";
27
+ import { join, dirname } from "path";
28
+ import { fileURLToPath } from "url";
29
+ import { homedir } from "os";
30
+
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+ const COMET_BRIDGE_ROOT = process.env.COMET_BRIDGE_ROOT || join(__dirname, "..");
34
+
35
+ const PILOT_REQUIRED_CHECKS = ["comet-app", "cdp", "config", "output-dirs"];
36
+ const BROAD_REQUIRED_CHECKS = [
37
+ "comet-app",
38
+ "cdp",
39
+ "mcp",
40
+ "extension-bridge",
41
+ "config",
42
+ "output-dirs",
43
+ ];
44
+
45
+ // --- Telemetry (S070-T019) ---
46
+
47
+ const TELEMETRY_OUTPUT_BASE =
48
+ process.env.COMET_OUTPUT_BASE || join(homedir(), ".claude/comet-browser/output");
49
+ const TELEMETRY_LOG_PATH = join(TELEMETRY_OUTPUT_BASE, "audit", "telemetry-events.jsonl");
50
+ const AUDIT_SESSION_BASE =
51
+ process.env.BROWSER_AUDIT_PATH || join(homedir(), "equabot", "audit", "browser-sessions");
52
+
53
+ const VALID_ROUTES = ["mcp", "cli", "http", "ide-browser"];
54
+ const VALID_OUTCOMES = ["completed", "failed", "aborted"];
55
+ const VALID_OWNERSHIP_RESULTS = ["passed", "failed", "skipped"];
56
+
57
+ // --- Telemetry Event Recording (S070-T019) ---
58
+
59
+ /**
60
+ * Append a telemetry event to the JSONL log.
61
+ * Other modules (cursor-comet.mjs, auto.mjs, tab-safety.mjs) call this to record
62
+ * route usage, fallback triggers, ownership check results, and run completions.
63
+ *
64
+ * @param {{
65
+ * type: 'route' | 'fallback' | 'ownership' | 'completion',
66
+ * route?: string,
67
+ * channel?: string,
68
+ * fallbackUsed?: boolean,
69
+ * fallbackReason?: string,
70
+ * ownershipResult?: 'passed' | 'failed' | 'skipped',
71
+ * ownershipAction?: string,
72
+ * agentId?: string,
73
+ * taskThreadId?: string,
74
+ * outcome?: 'completed' | 'failed' | 'aborted',
75
+ * runId?: string,
76
+ * durationMs?: number,
77
+ * metadata?: object,
78
+ * }} event
79
+ */
80
+ export function recordTelemetryEvent(event) {
81
+ if (!event || !event.type) return;
82
+ const entry = {
83
+ ts: new Date().toISOString(),
84
+ ...event,
85
+ };
86
+ try {
87
+ const dir = dirname(TELEMETRY_LOG_PATH);
88
+ mkdirSync(dir, { recursive: true });
89
+ const line = JSON.stringify(entry) + "\n";
90
+ writeFileSync(TELEMETRY_LOG_PATH, line, { flag: "a" });
91
+ } catch {
92
+ // telemetry is best-effort; never block the caller
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Read and parse telemetry events from the JSONL log.
98
+ * @param {{ maxAgeDays?: number, limit?: number }} [opts]
99
+ * @returns {object[]}
100
+ */
101
+ function readTelemetryEvents(opts = {}) {
102
+ const maxAgeDays = opts.maxAgeDays ?? 7;
103
+ const limit = opts.limit ?? 5000;
104
+ if (!existsSync(TELEMETRY_LOG_PATH)) return [];
105
+
106
+ let raw;
107
+ try {
108
+ raw = readFileSync(TELEMETRY_LOG_PATH, "utf-8");
109
+ } catch {
110
+ return [];
111
+ }
112
+
113
+ const cutoff = new Date(Date.now() - maxAgeDays * 86_400_000).toISOString();
114
+ const events = [];
115
+ for (const line of raw.split("\n")) {
116
+ if (!line.trim()) continue;
117
+ try {
118
+ const evt = JSON.parse(line);
119
+ if (evt.ts && evt.ts >= cutoff) events.push(evt);
120
+ } catch {
121
+ // skip malformed lines
122
+ }
123
+ if (events.length >= limit) break;
124
+ }
125
+ return events;
126
+ }
127
+
128
+ /**
129
+ * Read recent audit session metadata for completion stats enrichment.
130
+ * @param {{ limit?: number }} [opts]
131
+ * @returns {object[]}
132
+ */
133
+ function readRecentAuditSessions(opts = {}) {
134
+ const limit = opts.limit ?? 50;
135
+ if (!existsSync(AUDIT_SESSION_BASE)) return [];
136
+
137
+ let dirs;
138
+ try {
139
+ dirs = readdirSync(AUDIT_SESSION_BASE, { withFileTypes: true })
140
+ .filter((d) => d.isDirectory())
141
+ .map((d) => d.name)
142
+ .sort()
143
+ .reverse()
144
+ .slice(0, limit);
145
+ } catch {
146
+ return [];
147
+ }
148
+
149
+ const sessions = [];
150
+ for (const dir of dirs) {
151
+ const metaPath = join(AUDIT_SESSION_BASE, dir, "metadata.json");
152
+ if (!existsSync(metaPath)) continue;
153
+ try {
154
+ sessions.push(JSON.parse(readFileSync(metaPath, "utf-8")));
155
+ } catch {
156
+ // skip corrupt metadata
157
+ }
158
+ }
159
+ return sessions;
160
+ }
161
+
162
+ /**
163
+ * Aggregate telemetry events and audit sessions into an operational summary.
164
+ * Per Spec 070 S070-T019: route used, fallback status, ownership checks,
165
+ * and completion stats.
166
+ *
167
+ * @param {{ maxAgeDays?: number }} [opts]
168
+ * @returns {object} Telemetry summary
169
+ */
170
+ export function collectTelemetrySummary(opts = {}) {
171
+ const events = readTelemetryEvents(opts);
172
+ const sessions = readRecentAuditSessions();
173
+
174
+ const routeCounts = {};
175
+ for (const r of VALID_ROUTES) routeCounts[r] = 0;
176
+ let routeTotal = 0;
177
+
178
+ let fallbackTriggered = 0;
179
+ let fallbackTotal = 0;
180
+ const fallbackReasons = {};
181
+
182
+ const ownershipCounts = {};
183
+ for (const r of VALID_OWNERSHIP_RESULTS) ownershipCounts[r] = 0;
184
+ let ownershipTotal = 0;
185
+
186
+ const completionCounts = {};
187
+ for (const o of VALID_OUTCOMES) completionCounts[o] = 0;
188
+ let completionTotal = 0;
189
+ const durations = [];
190
+
191
+ for (const evt of events) {
192
+ switch (evt.type) {
193
+ case "route":
194
+ if (evt.route && VALID_ROUTES.includes(evt.route)) {
195
+ routeCounts[evt.route]++;
196
+ routeTotal++;
197
+ }
198
+ break;
199
+ case "fallback":
200
+ fallbackTotal++;
201
+ if (evt.fallbackUsed) {
202
+ fallbackTriggered++;
203
+ const reason = evt.fallbackReason || "unknown";
204
+ fallbackReasons[reason] = (fallbackReasons[reason] || 0) + 1;
205
+ }
206
+ break;
207
+ case "ownership":
208
+ if (evt.ownershipResult && VALID_OWNERSHIP_RESULTS.includes(evt.ownershipResult)) {
209
+ ownershipCounts[evt.ownershipResult]++;
210
+ ownershipTotal++;
211
+ }
212
+ break;
213
+ case "completion":
214
+ if (evt.outcome && VALID_OUTCOMES.includes(evt.outcome)) {
215
+ completionCounts[evt.outcome]++;
216
+ completionTotal++;
217
+ if (typeof evt.durationMs === "number") durations.push(evt.durationMs);
218
+ }
219
+ break;
220
+ }
221
+ }
222
+
223
+ let auditSessionStats = { total: 0, completed: 0, failed: 0, active: 0, other: 0 };
224
+ for (const s of sessions) {
225
+ auditSessionStats.total++;
226
+ if (s.status === "completed" && s.outcome === "success") auditSessionStats.completed++;
227
+ else if (s.status === "completed" && s.outcome === "failed") auditSessionStats.failed++;
228
+ else if (s.status === "failed") auditSessionStats.failed++;
229
+ else if (s.status === "active") auditSessionStats.active++;
230
+ else auditSessionStats.other++;
231
+ }
232
+
233
+ const avgDurationMs =
234
+ durations.length > 0
235
+ ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
236
+ : null;
237
+
238
+ return {
239
+ period: {
240
+ maxAgeDays: opts.maxAgeDays ?? 7,
241
+ eventsAnalyzed: events.length,
242
+ auditSessionsAnalyzed: sessions.length,
243
+ },
244
+ routes: {
245
+ total: routeTotal,
246
+ counts: routeCounts,
247
+ primary:
248
+ routeTotal > 0 ? Object.entries(routeCounts).sort((a, b) => b[1] - a[1])[0][0] : null,
249
+ },
250
+ fallbacks: {
251
+ total: fallbackTotal,
252
+ triggered: fallbackTriggered,
253
+ rate: fallbackTotal > 0 ? +(fallbackTriggered / fallbackTotal).toFixed(3) : 0,
254
+ reasons: fallbackReasons,
255
+ },
256
+ ownership: {
257
+ total: ownershipTotal,
258
+ counts: ownershipCounts,
259
+ violationRate: ownershipTotal > 0 ? +(ownershipCounts.failed / ownershipTotal).toFixed(3) : 0,
260
+ },
261
+ completions: {
262
+ total: completionTotal,
263
+ counts: completionCounts,
264
+ successRate:
265
+ completionTotal > 0 ? +(completionCounts.completed / completionTotal).toFixed(3) : 0,
266
+ avgDurationMs,
267
+ },
268
+ auditSessions: auditSessionStats,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Run bootstrap checks by invoking comet-bootstrap.mjs --json and parsing results.
274
+ * @param {{ port?: number, autoStart?: boolean, cometBridgeRoot?: string }} [opts]
275
+ * @returns {{ bootstrapReport: object, raw: string, checks: Array }}
276
+ */
277
+ export function runBootstrapChecks(opts = {}) {
278
+ const root = opts.cometBridgeRoot || COMET_BRIDGE_ROOT;
279
+ const scriptsDir = join(root, "scripts");
280
+ const args = [join(scriptsDir, "comet-bootstrap.mjs"), "--json"];
281
+ if (opts.port) args.push("--port", String(opts.port));
282
+ if (opts.autoStart) args.push("--auto-start");
283
+
284
+ const result = spawnSync(process.execPath, args, {
285
+ cwd: root,
286
+ encoding: "utf-8",
287
+ timeout: 30_000,
288
+ env: { ...process.env, COMET_BRIDGE_ROOT: root },
289
+ });
290
+
291
+ const raw = (result.stdout || "").trim();
292
+ let bootstrapReport;
293
+ try {
294
+ bootstrapReport = JSON.parse(raw);
295
+ } catch {
296
+ bootstrapReport = {
297
+ timestamp: new Date().toISOString(),
298
+ overall: "FAIL",
299
+ checks: [
300
+ {
301
+ id: "bootstrap-parse",
302
+ status: "FAIL",
303
+ critical: true,
304
+ message: `Could not parse bootstrap output: ${raw.slice(0, 200)}`,
305
+ },
306
+ ],
307
+ summary: { total: 1, passed: 0, failed: 1, warnings: 0, skipped: 0 },
308
+ };
309
+ }
310
+
311
+ return { bootstrapReport, raw, checks: bootstrapReport.checks || [] };
312
+ }
313
+
314
+ /**
315
+ * Assess two-tier readiness from bootstrap check results.
316
+ * @param {Array<{id: string, status: string}>} checks
317
+ * @returns {{ pilotReady: boolean, broadReady: boolean, pilotGaps: string[], broadGaps: string[] }}
318
+ */
319
+ export function assessReadinessTier(checks) {
320
+ const checkMap = new Map(checks.map((c) => [c.id, c.status]));
321
+
322
+ const pilotGaps = [];
323
+ for (const id of PILOT_REQUIRED_CHECKS) {
324
+ const status = checkMap.get(id);
325
+ if (!status || status === "FAIL") {
326
+ pilotGaps.push(id);
327
+ }
328
+ }
329
+
330
+ const broadGaps = [];
331
+ for (const id of BROAD_REQUIRED_CHECKS) {
332
+ const status = checkMap.get(id);
333
+ if (!status || status === "FAIL" || status === "WARN") {
334
+ broadGaps.push(id);
335
+ }
336
+ }
337
+
338
+ return {
339
+ pilotReady: pilotGaps.length === 0,
340
+ broadReady: broadGaps.length === 0,
341
+ pilotGaps,
342
+ broadGaps,
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Determine which commands are safe to run given current readiness.
348
+ * Maps to fallback-policy.md §3 degradation matrix.
349
+ * @param {Array<{id: string, status: string}>} checks
350
+ * @returns {{ allowed: string[], blocked: string[], warnings: string[] }}
351
+ */
352
+ export function assessCapabilities(checks) {
353
+ const checkMap = new Map(checks.map((c) => [c.id, c.status]));
354
+ const allowed = [];
355
+ const blocked = [];
356
+ const warnings = [];
357
+
358
+ const cdpOk = checkMap.get("cdp") === "PASS";
359
+ const mcpOk = checkMap.get("mcp") === "PASS";
360
+ const extOk = checkMap.get("extension-bridge") === "PASS";
361
+ const configOk = checkMap.get("config") === "PASS";
362
+ const appOk = checkMap.get("comet-app") === "PASS";
363
+
364
+ if (cdpOk && appOk && configOk) {
365
+ allowed.push(
366
+ "session",
367
+ "browse",
368
+ "search",
369
+ "scrape",
370
+ "interact",
371
+ "screenshot",
372
+ "pdf",
373
+ "network",
374
+ "monitor",
375
+ "keepalive",
376
+ "auto"
377
+ );
378
+ } else if (!cdpOk) {
379
+ blocked.push(
380
+ "session",
381
+ "browse",
382
+ "search",
383
+ "scrape",
384
+ "interact",
385
+ "screenshot",
386
+ "pdf",
387
+ "network",
388
+ "monitor",
389
+ "keepalive",
390
+ "auto"
391
+ );
392
+ } else {
393
+ blocked.push("Core browser commands (missing app binary or config)");
394
+ }
395
+
396
+ if (cdpOk && extOk) {
397
+ allowed.push("tab-groups (create/list/update/delete)");
398
+ allowed.push("multi-agent isolation (isolateGroups)");
399
+ } else if (cdpOk && !extOk) {
400
+ blocked.push("tab-groups (create/list/update/delete)");
401
+ blocked.push("multi-agent isolation (isolateGroups)");
402
+ warnings.push("Extension bridge not loaded — tab group and multi-agent isolation unavailable");
403
+ }
404
+
405
+ if (cdpOk && !extOk) {
406
+ // Title-prefix ownership still works without extension (fallback-policy §2.2)
407
+ allowed.push("multi-agent title-prefix ownership (degraded)");
408
+ }
409
+
410
+ if (!mcpOk) {
411
+ warnings.push("comet-mcp not built — MCP tools unavailable, CLI fallback required");
412
+ } else {
413
+ allowed.push("mcp-tools (comet_connect, comet_ask, comet_poll, etc.)");
414
+ }
415
+
416
+ // Lifecycle sync is best-effort; never blocks execution (fallback-policy §2.5)
417
+ allowed.push("lifecycle-sync (best-effort)");
418
+
419
+ return { allowed, blocked, warnings };
420
+ }
421
+
422
+ /**
423
+ * Derive boolean capability flags from a readiness checks array.
424
+ * Accepts either { checks: [...] } or a full report with { bootstrap: { checks: [...] } }.
425
+ * Used by tests and other modules for quick gating decisions.
426
+ *
427
+ * @param {{ checks?: Array, bootstrap?: { checks: Array } }} report
428
+ * @returns {{ isCoreReady: boolean, isTabGroupReady: boolean, isMCPReady: boolean, isMultiAgentReady: boolean }}
429
+ */
430
+ export function getCapabilityMatrix(report) {
431
+ const checks = report?.checks || report?.bootstrap?.checks || [];
432
+ const checkMap = new Map(checks.map((c) => [c.id, c.status]));
433
+
434
+ const cometAppOk = checkMap.get("comet-app") === "PASS";
435
+ const configOk = checkMap.get("config") === "PASS";
436
+ const outputOk = checkMap.get("output-dirs") === "PASS" || checkMap.get("output-dirs") === "WARN";
437
+ const cdpOk = checkMap.get("cdp") === "PASS";
438
+ const mcpOk = checkMap.get("mcp") === "PASS";
439
+ const extOk = checkMap.get("extension-bridge") === "PASS";
440
+
441
+ const isCoreReady = cometAppOk && configOk && outputOk;
442
+ const isTabGroupReady = cdpOk && extOk;
443
+ const isMCPReady = mcpOk;
444
+ const isMultiAgentReady = isCoreReady && cdpOk && isTabGroupReady;
445
+
446
+ return { isCoreReady, isTabGroupReady, isMCPReady, isMultiAgentReady };
447
+ }
448
+
449
+ /**
450
+ * Alias: run bootstrap checks and return structured report.
451
+ * @param {{ cometBridgeRoot?: string, port?: number }} [opts]
452
+ * @returns {object} Readiness report from generateReadinessReport
453
+ */
454
+ export function runReadinessChecks(opts = {}) {
455
+ return generateReadinessReport({ ...opts, persist: false, includeTelemetry: false });
456
+ }
457
+
458
+ /**
459
+ * Resolve the audit output directory for persisting reports.
460
+ * Falls back to <COMET_BRIDGE_ROOT>/output/audit if config is unavailable.
461
+ */
462
+ function resolveAuditDir() {
463
+ try {
464
+ // Attempt synchronous read of comet-config for the audit path.
465
+ // Config is ESM-only so we rely on env-based override or default path.
466
+ const envBase = process.env.COMET_OUTPUT_BASE;
467
+ if (envBase) return join(envBase, "audit");
468
+ } catch {
469
+ /* ignore */
470
+ }
471
+ return join(COMET_BRIDGE_ROOT, "output", "audit");
472
+ }
473
+
474
+ /**
475
+ * Generate the full readiness report.
476
+ *
477
+ * By default the report JSON is persisted to the audit directory for
478
+ * downstream consumption (S070-T019 telemetry, verification evidence).
479
+ * Pass `persist: false` to skip file writes.
480
+ *
481
+ * @param {{ port?: number, autoStart?: boolean, persist?: boolean, outPath?: string, includeTelemetry?: boolean, telemetryMaxAgeDays?: number }} [opts]
482
+ * @returns {object} Readiness report
483
+ */
484
+ export function generateReadinessReport(opts = {}) {
485
+ const { bootstrapReport } = runBootstrapChecks(opts);
486
+ const checks = bootstrapReport.checks || [];
487
+ const tier = assessReadinessTier(checks);
488
+ const capabilities = assessCapabilities(checks);
489
+
490
+ const readinessTier = tier.broadReady ? "Broad" : tier.pilotReady ? "Pilot" : "Not Ready";
491
+
492
+ const telemetry =
493
+ opts.includeTelemetry !== false
494
+ ? collectTelemetrySummary({ maxAgeDays: opts.telemetryMaxAgeDays })
495
+ : null;
496
+
497
+ const report = {
498
+ reportVersion: "2.0.0",
499
+ spec: "S070-T007+T019",
500
+ timestamp: new Date().toISOString(),
501
+ cometBridgeRoot: COMET_BRIDGE_ROOT,
502
+ bootstrap: bootstrapReport,
503
+ readiness: {
504
+ tier: readinessTier,
505
+ pilotReady: tier.pilotReady,
506
+ broadReady: tier.broadReady,
507
+ pilotGaps: tier.pilotGaps,
508
+ broadGaps: tier.broadGaps,
509
+ },
510
+ capabilities,
511
+ telemetry,
512
+ reportPath: null,
513
+ };
514
+
515
+ // Persist by default unless explicitly disabled
516
+ if (opts.persist !== false) {
517
+ try {
518
+ const outPath =
519
+ opts.outPath ||
520
+ (() => {
521
+ const auditDir = resolveAuditDir();
522
+ mkdirSync(auditDir, { recursive: true });
523
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
524
+ return join(auditDir, `readiness-report-${ts}.json`);
525
+ })();
526
+ mkdirSync(dirname(outPath), { recursive: true });
527
+ writeFileSync(outPath, JSON.stringify(report, null, 2));
528
+ report.reportPath = outPath;
529
+ } catch {
530
+ // Non-fatal: report generation succeeds even if file write fails
531
+ }
532
+ }
533
+
534
+ return report;
535
+ }
536
+
537
+ function printHumanReport(report) {
538
+ const tierIcon = { Broad: "\u2705", Pilot: "\u26A0\uFE0F", "Not Ready": "\u274C" };
539
+
540
+ console.log("");
541
+ console.log("=== Comet Toolkit Readiness Report ===");
542
+ console.log(`Spec: S070-T007 | Generated: ${report.timestamp}`);
543
+ console.log(`Root: ${report.cometBridgeRoot}`);
544
+ console.log("");
545
+
546
+ console.log("--- Bootstrap Checks ---");
547
+ const statusIcon = { PASS: "\u2705", FAIL: "\u274C", WARN: "\u26A0\uFE0F", SKIP: "\u23ED\uFE0F" };
548
+ for (const check of report.bootstrap.checks) {
549
+ console.log(` ${statusIcon[check.status] || "?"} [${check.id}] ${check.message}`);
550
+ }
551
+ console.log("");
552
+
553
+ console.log("--- Readiness Tier ---");
554
+ console.log(` ${tierIcon[report.readiness.tier] || "?"} ${report.readiness.tier}`);
555
+ if (report.readiness.tier === "Pilot") {
556
+ console.log(` Pilot: READY (core checks pass)`);
557
+ console.log(` Broad: NOT READY — gaps: ${report.readiness.broadGaps.join(", ")}`);
558
+ } else if (report.readiness.tier === "Not Ready") {
559
+ console.log(` Pilot: NOT READY — gaps: ${report.readiness.pilotGaps.join(", ")}`);
560
+ console.log(` Broad: NOT READY — gaps: ${report.readiness.broadGaps.join(", ")}`);
561
+ } else {
562
+ console.log(` Pilot: READY`);
563
+ console.log(` Broad: READY`);
564
+ }
565
+ console.log("");
566
+
567
+ console.log("--- Capability Matrix ---");
568
+ if (report.capabilities.allowed.length > 0) {
569
+ console.log(" Allowed:");
570
+ for (const cap of report.capabilities.allowed) console.log(` \u2705 ${cap}`);
571
+ }
572
+ if (report.capabilities.blocked.length > 0) {
573
+ console.log(" Blocked:");
574
+ for (const cap of report.capabilities.blocked) console.log(` \u274C ${cap}`);
575
+ }
576
+ if (report.capabilities.warnings.length > 0) {
577
+ console.log(" Warnings:");
578
+ for (const w of report.capabilities.warnings) console.log(` \u26A0\uFE0F ${w}`);
579
+ }
580
+ console.log("");
581
+
582
+ // --- Telemetry Summary (S070-T019) ---
583
+ if (report.telemetry) {
584
+ printTelemetrySummary(report.telemetry);
585
+ }
586
+
587
+ if (report.reportPath) {
588
+ console.log(`Report saved: ${report.reportPath}`);
589
+ console.log("");
590
+ }
591
+ }
592
+
593
+ function printTelemetrySummary(t) {
594
+ console.log("--- Operational Telemetry (S070-T019) ---");
595
+ console.log(
596
+ ` Period: last ${t.period.maxAgeDays} days | ${t.period.eventsAnalyzed} events | ${t.period.auditSessionsAnalyzed} audit sessions`
597
+ );
598
+ console.log("");
599
+
600
+ if (t.routes.total > 0) {
601
+ console.log(" Route Usage:");
602
+ for (const [route, count] of Object.entries(t.routes.counts)) {
603
+ if (count > 0) {
604
+ const pct = ((count / t.routes.total) * 100).toFixed(1);
605
+ const bar = "\u2588".repeat(Math.max(1, Math.round((count / t.routes.total) * 20)));
606
+ console.log(` ${route.padEnd(12)} ${String(count).padStart(4)} (${pct}%) ${bar}`);
607
+ }
608
+ }
609
+ console.log(` Primary route: ${t.routes.primary}`);
610
+ } else {
611
+ console.log(" Route Usage: no events recorded");
612
+ }
613
+ console.log("");
614
+
615
+ console.log(" Fallback Status:");
616
+ if (t.fallbacks.total > 0) {
617
+ const rateStr = (t.fallbacks.rate * 100).toFixed(1);
618
+ console.log(
619
+ ` Evaluated: ${t.fallbacks.total} | Triggered: ${t.fallbacks.triggered} (${rateStr}%)`
620
+ );
621
+ if (Object.keys(t.fallbacks.reasons).length > 0) {
622
+ console.log(" Reasons:");
623
+ for (const [reason, count] of Object.entries(t.fallbacks.reasons)) {
624
+ console.log(` - ${reason}: ${count}`);
625
+ }
626
+ }
627
+ } else {
628
+ console.log(" No fallback events recorded");
629
+ }
630
+ console.log("");
631
+
632
+ console.log(" Ownership Checks:");
633
+ if (t.ownership.total > 0) {
634
+ const vRateStr = (t.ownership.violationRate * 100).toFixed(1);
635
+ console.log(
636
+ ` Total: ${t.ownership.total} | Passed: ${t.ownership.counts.passed} | Failed: ${t.ownership.counts.failed} | Skipped: ${t.ownership.counts.skipped}`
637
+ );
638
+ console.log(` Violation rate: ${vRateStr}%`);
639
+ } else {
640
+ console.log(" No ownership check events recorded");
641
+ }
642
+ console.log("");
643
+
644
+ console.log(" Completion Stats:");
645
+ if (t.completions.total > 0) {
646
+ const sRateStr = (t.completions.successRate * 100).toFixed(1);
647
+ console.log(
648
+ ` Total: ${t.completions.total} | Completed: ${t.completions.counts.completed} | Failed: ${t.completions.counts.failed} | Aborted: ${t.completions.counts.aborted}`
649
+ );
650
+ console.log(` Success rate: ${sRateStr}%`);
651
+ if (t.completions.avgDurationMs !== null) {
652
+ const sec = (t.completions.avgDurationMs / 1000).toFixed(1);
653
+ console.log(` Avg duration: ${sec}s`);
654
+ }
655
+ } else {
656
+ console.log(" No completion events recorded");
657
+ }
658
+
659
+ if (t.auditSessions.total > 0) {
660
+ console.log("");
661
+ console.log(" Audit Sessions:");
662
+ console.log(
663
+ ` Total: ${t.auditSessions.total} | Completed: ${t.auditSessions.completed} | Failed: ${t.auditSessions.failed} | Active: ${t.auditSessions.active}`
664
+ );
665
+ }
666
+
667
+ console.log("");
668
+ }
669
+
670
+ // ---------- CLI entrypoint ----------
671
+
672
+ async function main() {
673
+ const argv = process.argv.slice(2);
674
+ const jsonFlag = argv.includes("--json");
675
+ const helpFlag = argv.includes("--help") || argv.includes("-h");
676
+ const noPersist = argv.includes("--no-persist");
677
+ const noTelemetry = argv.includes("--no-telemetry");
678
+ const saveIdx = argv.indexOf("--save");
679
+ const savePath = saveIdx !== -1 && saveIdx + 1 < argv.length ? argv[saveIdx + 1] : undefined;
680
+ const portIdx = argv.indexOf("--port");
681
+ const port =
682
+ portIdx !== -1 && portIdx + 1 < argv.length ? parseInt(argv[portIdx + 1], 10) : undefined;
683
+ const daysIdx = argv.indexOf("--telemetry-days");
684
+ const telemetryDays =
685
+ daysIdx !== -1 && daysIdx + 1 < argv.length ? parseInt(argv[daysIdx + 1], 10) : undefined;
686
+
687
+ if (helpFlag) {
688
+ console.error(`Comet Toolkit Readiness Report (Spec 070, S070-T007)
689
+
690
+ Usage:
691
+ node scripts/readiness-report.mjs [options]
692
+
693
+ Options:
694
+ --json Machine-readable JSON output
695
+ --save <path> Save report to a specific file (JSON)
696
+ --no-persist Skip automatic persistence to audit directory
697
+ --no-telemetry Omit operational telemetry section
698
+ --telemetry-days N Telemetry lookback window (default: 7)
699
+ --port <n> CDP port override
700
+ --auto-start Attempt Comet auto-start if CDP unreachable
701
+ --help Show this help
702
+
703
+ Readiness tiers (per clarification C3):
704
+ Pilot — core checks pass (app, CDP, config, output dirs)
705
+ Broad — all checks pass (including MCP build and extension bridge)
706
+
707
+ Exit codes:
708
+ 0 Broad-ready (all checks pass)
709
+ 1 Not ready (critical failures)
710
+ 2 Pilot-only (warnings for non-critical checks)`);
711
+ process.exit(0);
712
+ }
713
+
714
+ const autoStart = argv.includes("--auto-start");
715
+ const report = generateReadinessReport({
716
+ port,
717
+ autoStart,
718
+ persist: !noPersist,
719
+ outPath: savePath,
720
+ includeTelemetry: !noTelemetry,
721
+ telemetryMaxAgeDays: telemetryDays,
722
+ });
723
+
724
+ if (jsonFlag) {
725
+ console.log(JSON.stringify(report, null, 2));
726
+ } else {
727
+ printHumanReport(report);
728
+ }
729
+
730
+ if (report.readiness.tier === "Not Ready") process.exit(1);
731
+ if (report.readiness.tier === "Pilot") process.exit(2);
732
+ process.exit(0);
733
+ }
734
+
735
+ const isMainModule =
736
+ process.argv[1] &&
737
+ (process.argv[1].endsWith("readiness-report.mjs") ||
738
+ process.argv[1] === fileURLToPath(import.meta.url));
739
+
740
+ if (isMainModule) {
741
+ main();
742
+ }