@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,480 @@
1
+ /**
2
+ * Task Thread Aggregator — Phase A
3
+ *
4
+ * Reads all 5 local layers (lifecycle JSONL, session manifest, archive store,
5
+ * snapshots, alert log) and synthesises a CanonicalTaskThread for each known
6
+ * taskThreadId. Exposes the TaskThreadProvider interface so Phase B can swap
7
+ * the implementation for an equa-taskthreads core API client without touching
8
+ * any consumers (FR-040).
9
+ *
10
+ * Spec: specs/041-task-thread-sync/spec.md
11
+ * Plan: specs/041-task-thread-sync/plan.md §Layer Aggregator
12
+ */
13
+ import { readFile, readdir } from "fs/promises";
14
+ import { existsSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ import { resolveCanonicalStatus, assertValidTransition, InvalidTransitionError, } from "./task-thread-canonical.js";
18
+ import { archiveStore } from "./tab-group-archive.js";
19
+ import { loadBridgeConfig } from "./bridge-config.js";
20
+ // ─── File Paths ───────────────────────────────────────────────────────────────
21
+ const MANIFEST_PATH = join(homedir(), ".claude", "comet-browser", "session-manifest.json");
22
+ const LIFECYCLE_OUTBOX_PATH = join(homedir(), "equabot", "agent-chat", "outbox-comet.jsonl");
23
+ const CC_LIFECYCLE_URL = process.env.COMET_CC_LIFECYCLE_URL || "http://localhost:3001/command-center/api/comet/lifecycle";
24
+ const ORPHAN_THRESHOLD_MS = 120 * 60 * 1000; // 120 minutes
25
+ // ─── Layer Readers (T-004 through T-008) ──────────────────────────────────────
26
+ async function readArchiveLayer() {
27
+ try {
28
+ const entries = await archiveStore.loadAll();
29
+ const map = new Map();
30
+ for (const e of entries) {
31
+ map.set(e.taskThreadId, {
32
+ status: e.status,
33
+ title: e.title,
34
+ urls: e.urls,
35
+ archivedAt: e.archivedAt ?? null,
36
+ color: e.color,
37
+ });
38
+ }
39
+ return map;
40
+ }
41
+ catch {
42
+ return new Map();
43
+ }
44
+ }
45
+ async function readManifestLayer() {
46
+ try {
47
+ const raw = await readFile(MANIFEST_PATH, "utf-8");
48
+ const manifest = JSON.parse(raw);
49
+ const map = new Map();
50
+ const now = Date.now();
51
+ for (const entry of manifest.sessions) {
52
+ if (!entry.taskThreadId)
53
+ continue;
54
+ // ManifestEntry has no status field — infer from lastActivity (FR-033)
55
+ const age = now - entry.lastActivity;
56
+ const inferredStatus = age > ORPHAN_THRESHOLD_MS ? "orphaned" : "active";
57
+ map.set(entry.taskThreadId, {
58
+ status: inferredStatus,
59
+ agentId: entry.agentId,
60
+ taskGoal: entry.taskGoal ?? null,
61
+ createdAt: entry.createdAt,
62
+ lastActivity: entry.lastActivity,
63
+ sessionName: entry.sessionName ?? null,
64
+ sessionKey: entry.sessionKey,
65
+ });
66
+ }
67
+ return map;
68
+ }
69
+ catch {
70
+ return new Map();
71
+ }
72
+ }
73
+ async function readLifecycleLayer() {
74
+ const map = new Map();
75
+ // Parse JSONL outbox — every row with a `lifecycle` field is a lifecycle event
76
+ try {
77
+ const raw = await readFile(LIFECYCLE_OUTBOX_PATH, "utf-8");
78
+ for (const line of raw.split("\n")) {
79
+ if (!line.trim())
80
+ continue;
81
+ try {
82
+ const row = JSON.parse(line);
83
+ if (!row.lifecycle)
84
+ continue;
85
+ const threadId = row.thread ?? row.task;
86
+ if (!threadId)
87
+ continue;
88
+ const existing = map.get(threadId) ?? {
89
+ status: row.lifecycle.status ?? "dispatched",
90
+ runId: row.lifecycle.runId ?? null,
91
+ agentId: row.from ?? null,
92
+ events: [],
93
+ };
94
+ existing.events.push({
95
+ ts: row.ts ?? 0,
96
+ msg: row.msg ?? "",
97
+ lifecycle: row.lifecycle,
98
+ });
99
+ // Latest event wins for status (rows append-only, last = most recent)
100
+ existing.status = row.lifecycle.status;
101
+ if (row.lifecycle.runId)
102
+ existing.runId = row.lifecycle.runId;
103
+ map.set(threadId, existing);
104
+ }
105
+ catch { /* skip malformed lines */ }
106
+ }
107
+ }
108
+ catch { /* file absent — no lifecycle events */ }
109
+ // Best-effort: try CC API for live state; on failure, stick with JSONL-derived
110
+ try {
111
+ const resp = await fetch(`${CC_LIFECYCLE_URL}?action=list`, {
112
+ signal: AbortSignal.timeout(3000),
113
+ });
114
+ if (resp.ok) {
115
+ const data = await resp.json();
116
+ for (const run of data) {
117
+ if (!run.taskThreadId)
118
+ continue;
119
+ const existing = map.get(run.taskThreadId) ?? { status: run.status, runId: run.runId ?? null, agentId: run.agentId ?? null, events: [] };
120
+ existing.status = run.status; // live state wins over JSONL for active runs
121
+ map.set(run.taskThreadId, existing);
122
+ }
123
+ }
124
+ }
125
+ catch { /* CC offline — JSONL is the fallback */ }
126
+ return map;
127
+ }
128
+ async function readSnapshotLayer() {
129
+ const map = new Map();
130
+ const config = loadBridgeConfig();
131
+ const snapshotDirs = [
132
+ config.cleanup.snapshotDir,
133
+ join(homedir(), "equabot", "orchestration", "task-threads"),
134
+ ];
135
+ for (const dir of snapshotDirs) {
136
+ if (!existsSync(dir))
137
+ continue;
138
+ try {
139
+ const files = await readdir(dir);
140
+ for (const filename of files.filter((f) => f.endsWith(".json"))) {
141
+ const file = join(dir, filename);
142
+ try {
143
+ const raw = await readFile(file, "utf-8");
144
+ const snap = JSON.parse(raw);
145
+ if (!snap.taskThreadId)
146
+ continue;
147
+ // Keep the most recently captured snapshot per thread
148
+ const existing = map.get(snap.taskThreadId);
149
+ if (!existing || new Date(snap.capturedAt) > new Date(existing.capturedAt)) {
150
+ map.set(snap.taskThreadId, {
151
+ taskStatus: snap.taskStatus,
152
+ agentId: snap.agentId,
153
+ capturedAt: snap.capturedAt,
154
+ reason: snap.reason,
155
+ tabs: snap.tabs.map((t) => ({ url: t.url, title: t.title })),
156
+ });
157
+ }
158
+ }
159
+ catch { /* skip malformed files */ }
160
+ }
161
+ }
162
+ catch { /* skip unreadable directories */ }
163
+ }
164
+ return map;
165
+ }
166
+ async function readAlertLogLayer(manifestMap) {
167
+ const map = new Map();
168
+ // Build a reverse index: sessionKey → taskThreadId from manifest
169
+ const sessionKeyToThread = new Map();
170
+ for (const [threadId, rec] of manifestMap) {
171
+ sessionKeyToThread.set(rec.sessionKey, threadId);
172
+ }
173
+ try {
174
+ const config = loadBridgeConfig();
175
+ const raw = await readFile(config.alerts.logPath, "utf-8");
176
+ for (const line of raw.split("\n")) {
177
+ if (!line.trim())
178
+ continue;
179
+ try {
180
+ const event = JSON.parse(line);
181
+ const threadId = event.sessionKey ? sessionKeyToThread.get(event.sessionKey) : undefined;
182
+ if (!threadId)
183
+ continue;
184
+ const arr = map.get(threadId) ?? [];
185
+ arr.push(event);
186
+ map.set(threadId, arr);
187
+ }
188
+ catch { /* skip malformed lines */ }
189
+ }
190
+ }
191
+ catch { /* alert log absent */ }
192
+ return map;
193
+ }
194
+ // ─── History Timeline Builder (T-009) ─────────────────────────────────────────
195
+ function lifecycleActionToEventType(action) {
196
+ switch (action) {
197
+ case "start": return "LifecycleStarted";
198
+ case "complete": return "LifecycleCompleted";
199
+ case "abort": return "LifecycleAborted";
200
+ case "pause": return "LifecyclePaused";
201
+ case "resume": return "LifecycleResumed";
202
+ default: return "LifecycleUpdated";
203
+ }
204
+ }
205
+ function buildHistoryTimeline(threadId, lifecycle, manifest, archive, snapshot, alerts, canonicalStatus) {
206
+ const events = [];
207
+ // Lifecycle events
208
+ if (lifecycle) {
209
+ for (const le of lifecycle.events) {
210
+ events.push({
211
+ id: `lifecycle:${le.lifecycle.runId ?? threadId}:${le.ts}`,
212
+ timestamp: new Date(le.ts * 1000).toISOString(),
213
+ type: lifecycleActionToEventType(le.lifecycle.action),
214
+ layer: "lifecycle",
215
+ summary: le.msg || `Lifecycle ${le.lifecycle.action}`,
216
+ payload: { ...le.lifecycle },
217
+ });
218
+ }
219
+ }
220
+ // Session manifest registration
221
+ if (manifest) {
222
+ events.push({
223
+ id: `manifest:${manifest.sessionKey}:created`,
224
+ timestamp: new Date(manifest.createdAt).toISOString(),
225
+ type: "SessionRegistered",
226
+ layer: "session-manifest",
227
+ summary: `Session registered — agent=${manifest.agentId}${manifest.taskGoal ? `, goal="${manifest.taskGoal.slice(0, 60)}"` : ""}`,
228
+ payload: { sessionKey: manifest.sessionKey, agentId: manifest.agentId, status: manifest.status },
229
+ });
230
+ if (manifest.status === "orphaned") {
231
+ events.push({
232
+ id: `manifest:${manifest.sessionKey}:orphaned`,
233
+ timestamp: new Date(manifest.lastActivity).toISOString(),
234
+ type: "OrphanDetected",
235
+ layer: "session-manifest",
236
+ summary: "Session went orphaned (agent lost heartbeat)",
237
+ payload: { sessionKey: manifest.sessionKey, lastActivity: manifest.lastActivity },
238
+ });
239
+ }
240
+ }
241
+ // Archive events
242
+ if (archive) {
243
+ if (archive.archivedAt) {
244
+ events.push({
245
+ id: `archive:${threadId}:archived`,
246
+ timestamp: archive.archivedAt,
247
+ type: "Archived",
248
+ layer: "archive",
249
+ summary: `Tab group archived — "${archive.title}" (${archive.urls.length} tabs)`,
250
+ payload: { title: archive.title, urls: archive.urls, color: archive.color },
251
+ });
252
+ }
253
+ }
254
+ // Snapshot events
255
+ if (snapshot) {
256
+ events.push({
257
+ id: `snapshot:${threadId}:${snapshot.capturedAt}`,
258
+ timestamp: snapshot.capturedAt,
259
+ type: "SnapshotCaptured",
260
+ layer: "snapshot",
261
+ summary: `Snapshot captured — reason=${snapshot.reason}, status=${snapshot.taskStatus}`,
262
+ payload: { reason: snapshot.reason, taskStatus: snapshot.taskStatus, tabCount: snapshot.tabs.length },
263
+ });
264
+ }
265
+ // Alert log events
266
+ if (alerts) {
267
+ for (const alert of alerts) {
268
+ events.push({
269
+ id: `alert:${alert.id}`,
270
+ timestamp: alert.timestamp,
271
+ type: alert.type === "ORPHAN_DETECTED" || alert.type === "ORPHAN_REAPED"
272
+ ? "OrphanDetected"
273
+ : "AlertFired",
274
+ layer: "alert-log",
275
+ summary: `[${alert.severity}] ${alert.message}`,
276
+ payload: { alertType: alert.type, severity: alert.severity, context: alert.context },
277
+ });
278
+ }
279
+ }
280
+ // Sort ascending by timestamp
281
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
282
+ // Mark superseded events (FR-009):
283
+ // If a lifecycle terminal state exists, any non-lifecycle active-state events are superseded
284
+ const lifecycleIsTerminal = lifecycle &&
285
+ (lifecycle.status === "completed" || lifecycle.status === "aborted");
286
+ if (lifecycleIsTerminal) {
287
+ for (const ev of events) {
288
+ if (ev.layer !== "lifecycle") {
289
+ const activeTypes = ["SessionRegistered", "LifecycleStarted"];
290
+ if (activeTypes.includes(ev.type)) {
291
+ ev.superseded = true;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ return events;
297
+ }
298
+ // ─── Thread Assembler (T-010) ─────────────────────────────────────────────────
299
+ function assembleThread(threadId, lifecycle, manifest, archive, snapshot, alerts, stale) {
300
+ const layerStates = {
301
+ lifecycle: lifecycle?.status ?? null,
302
+ snapshot: snapshot?.taskStatus ?? null,
303
+ sessionManifest: manifest?.status ?? null,
304
+ archive: archive?.status ?? null,
305
+ alertLog: alerts && alerts.length > 0 ? alerts[alerts.length - 1].type : null,
306
+ };
307
+ const status = resolveCanonicalStatus(layerStates);
308
+ const history = buildHistoryTimeline(threadId, lifecycle, manifest, archive, snapshot, alerts, status);
309
+ // Title: prefer archive title, then session name, then taskGoal prefix, then threadId
310
+ const title = archive?.title ??
311
+ manifest?.sessionName ??
312
+ (manifest?.taskGoal ? manifest.taskGoal.slice(0, 60) : null) ??
313
+ threadId;
314
+ // createdAt: earliest known event
315
+ const createdAt = history.length > 0
316
+ ? history[0].timestamp
317
+ : manifest
318
+ ? new Date(manifest.createdAt).toISOString()
319
+ : new Date().toISOString();
320
+ // terminalAt: latest event if terminal
321
+ const terminalAt = status === "Completed" || status === "Aborted" || status === "Archived"
322
+ ? (history.length > 0 ? history[history.length - 1].timestamp : null)
323
+ : null;
324
+ // agentId: from lifecycle, then manifest, then snapshot
325
+ const agentId = lifecycle?.agentId ??
326
+ manifest?.agentId ??
327
+ snapshot?.agentId ??
328
+ null;
329
+ // DRI: from bridge config orchestratorAgentId (nullable in Phase A, OQ-3)
330
+ const config = loadBridgeConfig();
331
+ const dri = config.roles.orchestratorAgentId ?? null;
332
+ // tabs: from archive urls (TabGroupArchiveEntry.urls is {url,title}[]) or snapshot tabs
333
+ const tabs = archive?.urls.map((t) => t.url) ??
334
+ snapshot?.tabs.map((t) => t.url) ??
335
+ [];
336
+ const externalLinks = {
337
+ drive: null,
338
+ orgCharter: null,
339
+ linear: null,
340
+ };
341
+ return {
342
+ taskThreadId: threadId,
343
+ title,
344
+ status,
345
+ agentId,
346
+ dri,
347
+ createdAt,
348
+ terminalAt,
349
+ taskGoal: manifest?.taskGoal ?? null,
350
+ tabs,
351
+ externalLinks,
352
+ history,
353
+ stale,
354
+ };
355
+ }
356
+ // ─── Local Task Thread Aggregator (T-010, T-011) ──────────────────────────────
357
+ export class LocalTaskThreadAggregator {
358
+ async listAll() {
359
+ const [archiveMap, manifestMap, lifecycleMap, snapshotMap] = await Promise.allSettled([
360
+ readArchiveLayer(),
361
+ readManifestLayer(),
362
+ readLifecycleLayer(),
363
+ readSnapshotLayer(),
364
+ ]);
365
+ const stale = archiveMap.status === "rejected" ||
366
+ manifestMap.status === "rejected" ||
367
+ lifecycleMap.status === "rejected" ||
368
+ snapshotMap.status === "rejected";
369
+ const archive = archiveMap.status === "fulfilled" ? archiveMap.value : new Map();
370
+ const manifest = manifestMap.status === "fulfilled" ? manifestMap.value : new Map();
371
+ const lifecycle = lifecycleMap.status === "fulfilled" ? lifecycleMap.value : new Map();
372
+ const snapshot = snapshotMap.status === "fulfilled" ? snapshotMap.value : new Map();
373
+ const alertMap = await readAlertLogLayer(manifest).catch(() => new Map());
374
+ // Union of all known thread IDs across all layers (FR-010)
375
+ const allIds = new Set([
376
+ ...archive.keys(),
377
+ ...manifest.keys(),
378
+ ...lifecycle.keys(),
379
+ ...snapshot.keys(),
380
+ ...alertMap.keys(),
381
+ ]);
382
+ // Assemble concurrently, bounded to 10 (FR-036)
383
+ const ids = [...allIds];
384
+ const results = [];
385
+ for (let i = 0; i < ids.length; i += 10) {
386
+ const batch = ids.slice(i, i + 10);
387
+ const assembled = batch.map((id) => assembleThread(id, lifecycle.get(id), manifest.get(id), archive.get(id), snapshot.get(id), alertMap.get(id), stale));
388
+ results.push(...assembled);
389
+ }
390
+ return results;
391
+ }
392
+ async get(id) {
393
+ const [archiveMap, manifestMap, lifecycleMap, snapshotMap] = await Promise.allSettled([
394
+ readArchiveLayer(),
395
+ readManifestLayer(),
396
+ readLifecycleLayer(),
397
+ readSnapshotLayer(),
398
+ ]);
399
+ const stale = archiveMap.status === "rejected" ||
400
+ manifestMap.status === "rejected" ||
401
+ lifecycleMap.status === "rejected" ||
402
+ snapshotMap.status === "rejected";
403
+ const archive = archiveMap.status === "fulfilled" ? archiveMap.value.get(id) : undefined;
404
+ const manifest = manifestMap.status === "fulfilled" ? manifestMap.value.get(id) : undefined;
405
+ const lifecycle = lifecycleMap.status === "fulfilled" ? lifecycleMap.value.get(id) : undefined;
406
+ const snapshot = snapshotMap.status === "fulfilled" ? snapshotMap.value.get(id) : undefined;
407
+ const manifestMap2 = manifestMap.status === "fulfilled" ? manifestMap.value : new Map();
408
+ const alertMap = await readAlertLogLayer(manifestMap2).catch(() => new Map());
409
+ const alerts = alertMap.get(id);
410
+ // If no layer knows about this thread, return null
411
+ if (!archive && !manifest && !lifecycle && !snapshot && !alerts)
412
+ return null;
413
+ return assembleThread(id, lifecycle, manifest, archive, snapshot, alerts, stale);
414
+ }
415
+ async executeTransition(id, action, reason) {
416
+ // Fetch current state first
417
+ const thread = await this.get(id);
418
+ if (!thread) {
419
+ throw new Error(`Task thread "${id}" not found`);
420
+ }
421
+ // Validate transition (FR-019)
422
+ assertValidTransition(thread.status, action);
423
+ // Map action to lifecycle endpoint call (FR-018)
424
+ const lifecycleAction = {
425
+ "Start": "start",
426
+ "Mark Complete": "complete",
427
+ "Abort": "abort",
428
+ "Pause": "pause",
429
+ "Resume": "resume",
430
+ "Archive": null, // handled via archive store, not lifecycle
431
+ }[action];
432
+ if (lifecycleAction !== null && lifecycleAction !== undefined) {
433
+ // Call the lifecycle endpoint (best-effort — CC may be offline)
434
+ try {
435
+ const payload = { action: lifecycleAction, runId: id };
436
+ if (reason)
437
+ payload.reason = reason;
438
+ const resp = await fetch(CC_LIFECYCLE_URL, {
439
+ method: "POST",
440
+ headers: { "Content-Type": "application/json" },
441
+ body: JSON.stringify(payload),
442
+ signal: AbortSignal.timeout(5000),
443
+ });
444
+ if (!resp.ok) {
445
+ const text = await resp.text();
446
+ throw new Error(`Lifecycle endpoint error ${resp.status}: ${text.slice(0, 200)}`);
447
+ }
448
+ }
449
+ catch (err) {
450
+ if (err instanceof InvalidTransitionError)
451
+ throw err;
452
+ // Partial failure: lifecycle call failed — do not corrupt canonical state (FR-020)
453
+ throw new Error(`Write failed: lifecycle endpoint unreachable. Canonical state unchanged. (${err instanceof Error ? err.message : err})`);
454
+ }
455
+ }
456
+ if (action === "Archive") {
457
+ // Archive via archive store — update status to archived
458
+ const archiveEntry = await archiveStore.load(id);
459
+ if (archiveEntry) {
460
+ archiveEntry.status = "archived";
461
+ archiveEntry.archivedAt = new Date().toISOString();
462
+ await archiveStore.save(archiveEntry);
463
+ }
464
+ }
465
+ // Derive the expected new status from the valid transitions state machine
466
+ const statusAfterAction = {
467
+ "Start": "Running",
468
+ "Mark Complete": "Completed",
469
+ "Abort": "Aborted",
470
+ "Pause": "Paused",
471
+ "Resume": "Running",
472
+ "Archive": "Archived",
473
+ };
474
+ const newStatus = statusAfterAction[action] ?? thread.status;
475
+ return { newStatus };
476
+ }
477
+ }
478
+ // Singleton — Phase B: replace with CoreAPITaskThreadProvider
479
+ export const aggregator = new LocalTaskThreadAggregator();
480
+ //# sourceMappingURL=task-thread-aggregator.js.map
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Canonical Task Thread Types & Status Rule Set — Phase A
3
+ *
4
+ * AUTHORITATIVE LOCATION (FR-004): This file is the single source of truth
5
+ * for the Phase A canonical status vocabulary, conflict-resolution precedence,
6
+ * and valid lifecycle transition rules for the Comet browser surface worktree.
7
+ *
8
+ * In Phase B this document MUST link to or be replaced by the canonical rule
9
+ * from equa-taskthreads spec 003 (core API).
10
+ *
11
+ * Spec: specs/041-task-thread-sync/spec.md
12
+ * Plan: specs/041-task-thread-sync/plan.md §Canonical Status Rule Set
13
+ */
14
+ /**
15
+ * The six canonical status values for a task thread.
16
+ * Provisional in Phase A; to be reconciled with equa-taskthreads spec 003
17
+ * before or during Phase B.
18
+ */
19
+ export type CanonicalStatus = "Dispatched" | "Running" | "Paused" | "Completed" | "Aborted" | "Archived";
20
+ /** Terminal statuses — once set by the lifecycle layer, cannot revert (FR-008) */
21
+ export declare const TERMINAL_STATUSES: Set<CanonicalStatus>;
22
+ export declare function isTerminal(status: CanonicalStatus): boolean;
23
+ export type LayerName = "lifecycle" | "session-manifest" | "archive" | "snapshot" | "alert-log";
24
+ export type HistoryEventType = "Dispatched" | "SessionRegistered" | "LifecycleStarted" | "LifecycleCompleted" | "LifecycleAborted" | "LifecyclePaused" | "LifecycleResumed" | "LifecycleUpdated" | "SnapshotCaptured" | "Archived" | "Restored" | "UserAction" | "AlertFired" | "MirrorSync" | "OrphanDetected";
25
+ export interface HistoryEvent {
26
+ /** Stable unique ID for this event (layer:source-id or generated) */
27
+ id: string;
28
+ /** ISO-8601 timestamp */
29
+ timestamp: string;
30
+ type: HistoryEventType;
31
+ layer: LayerName;
32
+ /** Human-readable summary of the event */
33
+ summary: string;
34
+ /** Raw source payload for audit purposes */
35
+ payload?: Record<string, unknown>;
36
+ /**
37
+ * True when a higher-priority layer contradicts this event's state reading.
38
+ * Superseded events are preserved in the timeline for audit (FR-009).
39
+ */
40
+ superseded?: boolean;
41
+ }
42
+ export interface ExternalLinks {
43
+ /** Google Drive folder URL, or null if not mirrored */
44
+ drive: string | null;
45
+ /** Org-Charter task URL, or null if not mirrored */
46
+ orgCharter: string | null;
47
+ /** Linear issue URL, or null if not mirrored */
48
+ linear: string | null;
49
+ }
50
+ /**
51
+ * The canonical representation of a task thread in the Comet browser surface.
52
+ * Implements the immutable 4-layer anatomy from equa-taskthreads Constitution Principle I:
53
+ * - Task Header: id, title, status, agentId, dri, createdAt, terminalAt
54
+ * - Thread Summary: taskGoal
55
+ * - Resource Links: tabs, externalLinks
56
+ * - Conversation: history
57
+ */
58
+ export interface CanonicalTaskThread {
59
+ taskThreadId: string;
60
+ title: string;
61
+ status: CanonicalStatus;
62
+ /** Agent currently or last owning this thread; null after terminal state */
63
+ agentId: string | null;
64
+ /** Human DRI — nullable in Phase A until equa-taskthreads spec 003 defines authoritative source */
65
+ dri: string | null;
66
+ /** ISO-8601 creation time (earliest event across all layers) */
67
+ createdAt: string;
68
+ /** ISO-8601 terminal time; null if not yet terminal */
69
+ terminalAt: string | null;
70
+ /** Task goal as described at session registration; null if unknown */
71
+ taskGoal: string | null;
72
+ /** Archived tab URLs for this thread */
73
+ tabs: string[];
74
+ externalLinks: ExternalLinks;
75
+ /** Merged history timeline across all layers, sorted ascending by timestamp */
76
+ history: HistoryEvent[];
77
+ /**
78
+ * True when one or more layers could not be read and data may be incomplete.
79
+ * The extension shows a staleness indicator when this is true (FR-016).
80
+ */
81
+ stale: boolean;
82
+ }
83
+ /**
84
+ * Maps each canonical status to the set of write actions the user may invoke
85
+ * from that state. The extension renders exactly these buttons (FR-017).
86
+ *
87
+ * Action names match the payload `action` field for POST /api/task-threads/:id/transition.
88
+ */
89
+ export declare const VALID_TRANSITIONS: Readonly<Record<CanonicalStatus, readonly string[]>>;
90
+ export interface LayerStates {
91
+ /**
92
+ * Phase A: raw status string from the lifecycle JSONL/CC API.
93
+ * Expected values: "started" | "dispatched" | "paused" | "completed" | "aborted" | null
94
+ */
95
+ lifecycle: string | null;
96
+ /**
97
+ * From TaskThreadSnapshot.taskStatus
98
+ * Expected values: "success" | "failed" | "abandoned" | "in-progress" | null
99
+ */
100
+ snapshot: string | null;
101
+ /**
102
+ * From ManifestEntry SessionStatus
103
+ * Expected values: "active" | "disconnecting" | "orphaned" | null
104
+ */
105
+ sessionManifest: string | null;
106
+ /**
107
+ * From TabGroupArchiveEntry.status
108
+ * Expected values: "saved" | "archived" | null
109
+ */
110
+ archive: string | null;
111
+ /**
112
+ * From alert log ErrorEventType for this thread
113
+ * Expected values: "ORPHAN_DETECTED" | "ORPHAN_REAPED" | other | null
114
+ */
115
+ alertLog: string | null;
116
+ }
117
+ /**
118
+ * Derives exactly one canonical status from the union of underlying layer states.
119
+ *
120
+ * Precedence (FR-006, FR-007):
121
+ * 1. Lifecycle layer (highest authority — Lifecycle wins)
122
+ * 2. Snapshot layer
123
+ * 3. Session manifest layer
124
+ * 4. Archive layer
125
+ * 5. Alert log layer
126
+ * 6. Unknown → Archived (safe display fallback)
127
+ *
128
+ * Terminal states (Completed, Aborted, Archived) are sticky once set by
129
+ * the lifecycle layer (FR-008).
130
+ */
131
+ export declare function resolveCanonicalStatus(layers: LayerStates): CanonicalStatus;
132
+ export declare class InvalidTransitionError extends Error {
133
+ readonly fromStatus: CanonicalStatus;
134
+ readonly action: string;
135
+ constructor(fromStatus: CanonicalStatus, action: string);
136
+ }
137
+ /**
138
+ * Throws InvalidTransitionError if the action is not valid from the given status.
139
+ * Does NOT perform the transition — call this before executing any write.
140
+ */
141
+ export declare function assertValidTransition(status: CanonicalStatus, action: string): void;
142
+ //# sourceMappingURL=task-thread-canonical.d.ts.map