gossipcat 0.6.4 → 0.6.6

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.
@@ -34010,8 +34010,8 @@ Keep it SHORT \u2014 under 30 lines. This is a working document, not a design do
34010
34010
  ]);
34011
34011
  const specContent = response.text || "";
34012
34012
  try {
34013
- const { mkdirSync: mkdirSync44, writeFileSync: writeFS } = require("fs");
34014
- mkdirSync44((0, import_path35.join)(this.projectRoot, ".gossip"), { recursive: true });
34013
+ const { mkdirSync: mkdirSync45, writeFileSync: writeFS } = require("fs");
34014
+ mkdirSync45((0, import_path35.join)(this.projectRoot, ".gossip"), { recursive: true });
34015
34015
  writeFS(specPath, specContent, "utf-8");
34016
34016
  } catch (err) {
34017
34017
  return { text: `Spec generated but failed to save: ${err.message}
@@ -35274,8 +35274,8 @@ message: Your question?
35274
35274
  }
35275
35275
  /** Start all worker agents (connect to relay) */
35276
35276
  async start() {
35277
- const { existsSync: existsSync73, readFileSync: readFileSync68 } = await import("fs");
35278
- const { join: join90 } = await import("path");
35277
+ const { existsSync: existsSync73, readFileSync: readFileSync69 } = await import("fs");
35278
+ const { join: join91 } = await import("path");
35279
35279
  for (const config2 of this.registry.getAll()) {
35280
35280
  if (config2.native) continue;
35281
35281
  if (this.workers.has(config2.id)) continue;
@@ -35293,8 +35293,8 @@ message: Your question?
35293
35293
  void 0,
35294
35294
  config2.key_ref
35295
35295
  );
35296
- const instructionsPath = join90(this.projectRoot, ".gossip", "agents", config2.id, "instructions.md");
35297
- const instructions = existsSync73(instructionsPath) ? readFileSync68(instructionsPath, "utf-8") : void 0;
35296
+ const instructionsPath = join91(this.projectRoot, ".gossip", "agents", config2.id, "instructions.md");
35297
+ const instructions = existsSync73(instructionsPath) ? readFileSync69(instructionsPath, "utf-8") : void 0;
35298
35298
  const enableWebSearch = config2.preset === "researcher" || config2.skills.includes("research");
35299
35299
  const worker = new WorkerAgent(config2.id, llm, this.relayUrl, ALL_TOOLS, instructions, enableWebSearch, this.relayApiKey, config2.maxToolTurns);
35300
35300
  await worker.start();
@@ -35491,8 +35491,8 @@ message: Your question?
35491
35491
  this.registry.register(config2);
35492
35492
  }
35493
35493
  async syncWorkers(keyProvider) {
35494
- const { existsSync: existsSync73, readFileSync: readFileSync68 } = await import("fs");
35495
- const { join: join90 } = await import("path");
35494
+ const { existsSync: existsSync73, readFileSync: readFileSync69 } = await import("fs");
35495
+ const { join: join91 } = await import("path");
35496
35496
  let added = 0;
35497
35497
  for (const ac of this.registry.getAll()) {
35498
35498
  if (ac.native) continue;
@@ -35509,8 +35509,8 @@ message: Your question?
35509
35509
  this.workers.delete(ac.id);
35510
35510
  }
35511
35511
  const llm = createProviderForAgent(ac.id, ac.provider, ac.model, key ?? void 0, ac.base_url, void 0, ac.key_ref);
35512
- const instructionsPath = join90(this.projectRoot, ".gossip", "agents", ac.id, "instructions.md");
35513
- const instructions = existsSync73(instructionsPath) ? readFileSync68(instructionsPath, "utf-8") : void 0;
35512
+ const instructionsPath = join91(this.projectRoot, ".gossip", "agents", ac.id, "instructions.md");
35513
+ const instructions = existsSync73(instructionsPath) ? readFileSync69(instructionsPath, "utf-8") : void 0;
35514
35514
  const enableWebSearch = ac.preset === "researcher" || ac.skills.includes("research");
35515
35515
  const worker = new WorkerAgent(ac.id, llm, this.relayUrl, ALL_TOOLS, instructions, enableWebSearch, this.relayApiKey, ac.maxToolTurns);
35516
35516
  await worker.start();
@@ -44211,18 +44211,163 @@ var init_api_chat = __esm({
44211
44211
  }
44212
44212
  });
44213
44213
 
44214
+ // packages/relay/src/dashboard/chat-store.ts
44215
+ function isMirrorFrameShaped(obj) {
44216
+ if (!obj || typeof obj !== "object") return false;
44217
+ const f = obj;
44218
+ return f.type === "mirror" && typeof f.chat_id === "string" && typeof f.role === "string" && typeof f.text === "string" && typeof f.ts === "string" && typeof f.id === "number";
44219
+ }
44220
+ var import_fs70, import_path78, CHAT_ID_RE, PERSIST_CAP, MAX_LINE_BYTES, NullChatStore, FileChatStore;
44221
+ var init_chat_store = __esm({
44222
+ "packages/relay/src/dashboard/chat-store.ts"() {
44223
+ "use strict";
44224
+ import_fs70 = require("fs");
44225
+ import_path78 = require("path");
44226
+ CHAT_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
44227
+ PERSIST_CAP = 1e3;
44228
+ MAX_LINE_BYTES = 256 * 1024;
44229
+ NullChatStore = class {
44230
+ append(_chatId, _frame) {
44231
+ }
44232
+ load(_chatId, _max) {
44233
+ return [];
44234
+ }
44235
+ drop(_chatId) {
44236
+ }
44237
+ dispose() {
44238
+ }
44239
+ };
44240
+ FileChatStore = class {
44241
+ constructor(chatDir) {
44242
+ this.chatDir = chatDir;
44243
+ }
44244
+ chatDir;
44245
+ /**
44246
+ * Per-chat append counter. Used to gate maybeRetain() calls so we do not
44247
+ * read the file on every append (O(1) amortized instead of O(N)):
44248
+ * - First append for a chatId in this process: one-time retention check
44249
+ * (bounds a large pre-existing file from a prior run).
44250
+ * - Subsequent appends: only call maybeRetain every PERSIST_CAP writes.
44251
+ */
44252
+ appendCounters = /* @__PURE__ */ new Map();
44253
+ isValidChatId(chatId) {
44254
+ return CHAT_ID_RE.test(chatId);
44255
+ }
44256
+ filePath(chatId) {
44257
+ return (0, import_path78.join)(this.chatDir, `${chatId}.jsonl`);
44258
+ }
44259
+ append(chatId, frame) {
44260
+ if (!this.isValidChatId(chatId)) return;
44261
+ if (chatId.startsWith("_")) return;
44262
+ try {
44263
+ (0, import_fs70.mkdirSync)(this.chatDir, { recursive: true });
44264
+ const line = JSON.stringify(frame) + "\n";
44265
+ (0, import_fs70.appendFileSync)(this.filePath(chatId), line, "utf8");
44266
+ const prev = this.appendCounters.get(chatId);
44267
+ const count = (prev ?? 0) + 1;
44268
+ this.appendCounters.set(chatId, count);
44269
+ const isFirst = prev === void 0;
44270
+ if (isFirst || count % PERSIST_CAP === 0) {
44271
+ this.maybeRetain(chatId);
44272
+ }
44273
+ } catch (err) {
44274
+ console.warn(`[chat-store] append failed for chatId=${chatId}:`, err);
44275
+ }
44276
+ }
44277
+ maybeRetain(chatId) {
44278
+ if (chatId.startsWith("_")) return;
44279
+ try {
44280
+ const fp = this.filePath(chatId);
44281
+ const content = (0, import_fs70.readFileSync)(fp, "utf8");
44282
+ const lines = content.split("\n").filter((l) => l.length > 0);
44283
+ if (lines.length < 2 * PERSIST_CAP) return;
44284
+ const kept = lines.slice(lines.length - PERSIST_CAP);
44285
+ const tmp = fp + ".tmp";
44286
+ (0, import_fs70.writeFileSync)(tmp, kept.join("\n") + "\n", "utf8");
44287
+ (0, import_fs70.renameSync)(tmp, fp);
44288
+ } catch (err) {
44289
+ this.cleanupStaleTmp(chatId);
44290
+ console.warn(`[chat-store] retain failed for chatId=${chatId}`, err);
44291
+ }
44292
+ }
44293
+ /**
44294
+ * Best-effort removal of a stale `<chatId>.jsonl.tmp` left by a crash between
44295
+ * maybeRetain's writeFileSync and renameSync (the live .jsonl is always
44296
+ * intact; only the tmp orphans). Called on cold load() so orphans from a
44297
+ * prior process run are reclaimed when the chat is next hydrated, and from
44298
+ * maybeRetain's catch. ENOENT (the normal case) is silent.
44299
+ */
44300
+ cleanupStaleTmp(chatId) {
44301
+ try {
44302
+ (0, import_fs70.unlinkSync)(this.filePath(chatId) + ".tmp");
44303
+ } catch (err) {
44304
+ if (err?.code !== "ENOENT") {
44305
+ console.warn(`[chat-store] stale .tmp cleanup failed for chatId=${chatId}:`, err);
44306
+ }
44307
+ }
44308
+ }
44309
+ load(chatId, max) {
44310
+ if (!this.isValidChatId(chatId)) return [];
44311
+ this.cleanupStaleTmp(chatId);
44312
+ let content;
44313
+ try {
44314
+ content = (0, import_fs70.readFileSync)(this.filePath(chatId), "utf8");
44315
+ } catch (err) {
44316
+ if (err?.code === "ENOENT") return [];
44317
+ console.warn(`[chat-store] load failed for chatId=${chatId}:`, err);
44318
+ return [];
44319
+ }
44320
+ const lines = content.split("\n").filter((l) => l.length > 0);
44321
+ const frames = [];
44322
+ for (const line of lines) {
44323
+ if (line.length > MAX_LINE_BYTES) {
44324
+ console.warn(`[chat-store] skipping oversized line (${line.length} bytes) for chatId=${chatId}`);
44325
+ continue;
44326
+ }
44327
+ try {
44328
+ const parsed = JSON.parse(line);
44329
+ if (isMirrorFrameShaped(parsed)) {
44330
+ frames.push(parsed);
44331
+ } else {
44332
+ console.warn(`[chat-store] skipping line with valid JSON but wrong MirrorFrame shape for chatId=${chatId}`);
44333
+ }
44334
+ } catch {
44335
+ console.warn(`[chat-store] skipping unparseable line for chatId=${chatId}`);
44336
+ }
44337
+ }
44338
+ if (frames.length <= max) return frames;
44339
+ return frames.slice(frames.length - max);
44340
+ }
44341
+ drop(chatId) {
44342
+ if (!this.isValidChatId(chatId)) return;
44343
+ try {
44344
+ (0, import_fs70.unlinkSync)(this.filePath(chatId));
44345
+ } catch (err) {
44346
+ if (err?.code !== "ENOENT") {
44347
+ console.warn(`[chat-store] drop failed for chatId=${chatId}:`, err);
44348
+ }
44349
+ }
44350
+ }
44351
+ dispose() {
44352
+ }
44353
+ };
44354
+ }
44355
+ });
44356
+
44214
44357
  // packages/relay/src/dashboard/api-mirror-events.ts
44215
44358
  var MIRROR_RING_MAX, MIRROR_RING_TTL_MS, MIRROR_SWEEP_INTERVAL_MS, MirrorEventStore;
44216
44359
  var init_api_mirror_events = __esm({
44217
44360
  "packages/relay/src/dashboard/api-mirror-events.ts"() {
44218
44361
  "use strict";
44362
+ init_chat_store();
44219
44363
  MIRROR_RING_MAX = 100;
44220
44364
  MIRROR_RING_TTL_MS = 2 * 60 * 60 * 1e3;
44221
44365
  MIRROR_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
44222
44366
  MirrorEventStore = class {
44223
- constructor(ringMax = MIRROR_RING_MAX, ttlMs = MIRROR_RING_TTL_MS, sweepIntervalMs = MIRROR_SWEEP_INTERVAL_MS) {
44367
+ constructor(ringMax = MIRROR_RING_MAX, ttlMs = MIRROR_RING_TTL_MS, sweepIntervalMs = MIRROR_SWEEP_INTERVAL_MS, store = new NullChatStore()) {
44224
44368
  this.ringMax = ringMax;
44225
44369
  this.ttlMs = ttlMs;
44370
+ this.store = store;
44226
44371
  if (sweepIntervalMs > 0) {
44227
44372
  this.sweepTimer = setInterval(() => this.sweep(Date.now()), sweepIntervalMs);
44228
44373
  this.sweepTimer.unref?.();
@@ -44232,18 +44377,44 @@ var init_api_mirror_events = __esm({
44232
44377
  ttlMs;
44233
44378
  rings = /* @__PURE__ */ new Map();
44234
44379
  sweepTimer = null;
44380
+ store;
44235
44381
  /**
44236
- * Allocate the next id for a chat_id WITHOUT pushing a frame. The hub stamps
44237
- * id + ts server-side, so it asks the store to mint the id, builds the frame,
44238
- * then pushes it. Keeping mint+push as one call (push) avoids a torn counter,
44239
- * so this is internal callers use push().
44382
+ * Allocate or hydrate a ring for a chat_id. On first access when the ring is
44383
+ * not in memory, loads persisted frames from the ChatStore so post-restart
44384
+ * reads see durable history and the id sequence continues (preventing false
44385
+ * restart-sentinels). Returns null when neither an in-memory ring exists NOR
44386
+ * any persisted frames were found — callers treat null as "no data."
44387
+ *
44388
+ * Renamed from `ensureRing` to `getOrHydrate` to reflect the new semantics.
44389
+ * push() uses a variant that always creates the ring (even if empty), since a
44390
+ * push always establishes a new ring. replaySlice/highestId use this
44391
+ * conditional form so they don't pollute ringCount() with empty rings on miss.
44392
+ */
44393
+ getOrHydrate(chatId, now) {
44394
+ const existing = this.rings.get(chatId);
44395
+ if (existing) return existing;
44396
+ const persisted = this.store.load(chatId, this.ringMax);
44397
+ if (persisted.length === 0) return null;
44398
+ const ring2 = {
44399
+ frames: persisted.slice(),
44400
+ // Continue the id sequence from the highest persisted id so push() does
44401
+ // not reset to 1 after a restart (preventing the false restart-sentinel).
44402
+ nextId: persisted[persisted.length - 1].id,
44403
+ touchedAt: now
44404
+ };
44405
+ this.rings.set(chatId, ring2);
44406
+ return ring2;
44407
+ }
44408
+ /**
44409
+ * Ensure a ring exists for a chat_id, creating it from persisted data or
44410
+ * fresh. Always returns a ring (never null). Used only by push() since a
44411
+ * push always establishes a ring.
44240
44412
  */
44241
44413
  ensureRing(chatId, now) {
44242
- let ring2 = this.rings.get(chatId);
44243
- if (!ring2) {
44244
- ring2 = { frames: [], nextId: 0, touchedAt: now };
44245
- this.rings.set(chatId, ring2);
44246
- }
44414
+ const hydrated = this.getOrHydrate(chatId, now);
44415
+ if (hydrated) return hydrated;
44416
+ const ring2 = { frames: [], nextId: 0, touchedAt: now };
44417
+ this.rings.set(chatId, ring2);
44247
44418
  return ring2;
44248
44419
  }
44249
44420
  /**
@@ -44266,6 +44437,7 @@ var init_api_mirror_events = __esm({
44266
44437
  ring2.frames.push(frame);
44267
44438
  while (ring2.frames.length > this.ringMax) ring2.frames.shift();
44268
44439
  ring2.touchedAt = now;
44440
+ this.store.append(chatId, frame);
44269
44441
  return frame;
44270
44442
  }
44271
44443
  /**
@@ -44275,7 +44447,7 @@ var init_api_mirror_events = __esm({
44275
44447
  * actively-observed stream isn't swept out from under a reconnecting client.
44276
44448
  */
44277
44449
  replaySlice(chatId, lastId, now = Date.now()) {
44278
- const ring2 = this.rings.get(chatId);
44450
+ const ring2 = this.getOrHydrate(chatId, now);
44279
44451
  if (!ring2) return [];
44280
44452
  ring2.touchedAt = now;
44281
44453
  if (lastId <= 0) return ring2.frames.slice();
@@ -44307,6 +44479,7 @@ var init_api_mirror_events = __esm({
44307
44479
  }
44308
44480
  const carried = src.frames.slice();
44309
44481
  this.rings.delete(fromChatId);
44482
+ this.store.drop(fromChatId);
44310
44483
  const transferred = [];
44311
44484
  for (const f of carried) {
44312
44485
  transferred.push(this.push(toChatId, f.role, f.text, now));
@@ -44321,7 +44494,7 @@ var init_api_mirror_events = __esm({
44321
44494
  * sentinel in api-bridge.handleStream).
44322
44495
  */
44323
44496
  highestId(chatId) {
44324
- const ring2 = this.rings.get(chatId);
44497
+ const ring2 = this.getOrHydrate(chatId, Date.now());
44325
44498
  if (!ring2 || ring2.frames.length === 0) return 0;
44326
44499
  return ring2.frames[ring2.frames.length - 1].id;
44327
44500
  }
@@ -44350,25 +44523,110 @@ var init_api_mirror_events = __esm({
44350
44523
  });
44351
44524
 
44352
44525
  // packages/relay/src/dashboard/api-bridge.ts
44526
+ function validateAskQuestions(raw) {
44527
+ if (!Array.isArray(raw) || raw.length === 0) {
44528
+ return { error: "questions must be a non-empty array" };
44529
+ }
44530
+ if (raw.length > ASK_MAX_QUESTIONS) {
44531
+ return { error: `too many questions (max ${ASK_MAX_QUESTIONS})` };
44532
+ }
44533
+ const out = [];
44534
+ for (let i = 0; i < raw.length; i++) {
44535
+ const q = raw[i];
44536
+ if (!q || typeof q !== "object") {
44537
+ return { error: `question ${i} must be an object` };
44538
+ }
44539
+ if (typeof q.header !== "string" || q.header.trim().length === 0) {
44540
+ return { error: `question ${i} header must be a non-empty string` };
44541
+ }
44542
+ if (q.header.length > ASK_MAX_HEADER) {
44543
+ return { error: `question ${i} header too long (max ${ASK_MAX_HEADER})` };
44544
+ }
44545
+ if (typeof q.question !== "string" || q.question.trim().length === 0) {
44546
+ return { error: `question ${i} question must be a non-empty string` };
44547
+ }
44548
+ if (q.question.length > ASK_MAX_QUESTION) {
44549
+ return { error: `question ${i} question too long (max ${ASK_MAX_QUESTION})` };
44550
+ }
44551
+ if (q.multiSelect !== void 0 && typeof q.multiSelect !== "boolean") {
44552
+ return { error: `question ${i} multiSelect must be a boolean` };
44553
+ }
44554
+ if (q.allowOther !== void 0 && typeof q.allowOther !== "boolean") {
44555
+ return { error: `question ${i} allowOther must be a boolean` };
44556
+ }
44557
+ if (!Array.isArray(q.options) || q.options.length === 0) {
44558
+ return { error: `question ${i} options must be a non-empty array` };
44559
+ }
44560
+ if (q.options.length > ASK_MAX_OPTIONS) {
44561
+ return { error: `question ${i} has too many options (max ${ASK_MAX_OPTIONS})` };
44562
+ }
44563
+ const options = [];
44564
+ const labels = /* @__PURE__ */ new Set();
44565
+ for (let j = 0; j < q.options.length; j++) {
44566
+ const o = q.options[j];
44567
+ if (!o || typeof o !== "object") {
44568
+ return { error: `question ${i} option ${j} must be an object` };
44569
+ }
44570
+ if (typeof o.label !== "string" || o.label.trim().length === 0) {
44571
+ return { error: `question ${i} option ${j} label must be a non-empty string` };
44572
+ }
44573
+ if (o.label.length > ASK_MAX_LABEL) {
44574
+ return { error: `question ${i} option ${j} label too long (max ${ASK_MAX_LABEL})` };
44575
+ }
44576
+ if (labels.has(o.label)) {
44577
+ return { error: `question ${i} has a duplicate option label` };
44578
+ }
44579
+ labels.add(o.label);
44580
+ const opt = { label: o.label };
44581
+ if (o.description !== void 0 && o.description !== null) {
44582
+ if (typeof o.description !== "string") {
44583
+ return { error: `question ${i} option ${j} description must be a string` };
44584
+ }
44585
+ if (o.description.length > ASK_MAX_DESCRIPTION) {
44586
+ return { error: `question ${i} option ${j} description too long (max ${ASK_MAX_DESCRIPTION})` };
44587
+ }
44588
+ opt.description = o.description;
44589
+ }
44590
+ options.push(opt);
44591
+ }
44592
+ out.push({
44593
+ questionId: `q${i}`,
44594
+ header: q.header,
44595
+ question: q.question,
44596
+ ...q.multiSelect === true ? { multiSelect: true } : {},
44597
+ options,
44598
+ ...q.allowOther === true ? { allowOther: true } : {}
44599
+ });
44600
+ }
44601
+ return { questions: out };
44602
+ }
44353
44603
  function isMirrorRole(v) {
44354
44604
  return typeof v === "string" && MIRROR_ROLES.includes(v);
44355
44605
  }
44356
44606
  function validateChatId(raw) {
44357
44607
  if (typeof raw !== "string") return null;
44358
44608
  const v = raw.trim();
44359
- if (!CHAT_ID_RE.test(v)) return null;
44609
+ if (!CHAT_ID_RE2.test(v)) return null;
44360
44610
  return v;
44361
44611
  }
44362
- var import_crypto26, MIRROR_ROLES, MIRROR_MAX_TEXT, MIRROR_MAX_FRAMES, CHAT_ID_RE, MAX_MESSAGE_LENGTH, MAX_BRIDGE_CLIENTS, KEEPALIVE_MS2, BACKPRESSURE_EVICT_THRESHOLD2, BridgeHub;
44612
+ var import_crypto26, ASK_MAX_QUESTIONS, ASK_MAX_OPTIONS, ASK_MAX_HEADER, ASK_MAX_QUESTION, ASK_MAX_LABEL, ASK_MAX_DESCRIPTION, ASK_MAX_OTHER, MIRROR_ROLES, MIRROR_MAX_TEXT, MIRROR_MAX_FRAMES, CHAT_ID_RE2, MAX_MESSAGE_LENGTH, MAX_BRIDGE_CLIENTS, KEEPALIVE_MS2, BACKPRESSURE_EVICT_THRESHOLD2, BridgeHub;
44363
44613
  var init_api_bridge = __esm({
44364
44614
  "packages/relay/src/dashboard/api-bridge.ts"() {
44365
44615
  "use strict";
44366
44616
  import_crypto26 = require("crypto");
44367
44617
  init_api_mirror_events();
44618
+ init_chat_store();
44619
+ ASK_MAX_QUESTIONS = 4;
44620
+ ASK_MAX_OPTIONS = 8;
44621
+ ASK_MAX_HEADER = 120;
44622
+ ASK_MAX_QUESTION = 600;
44623
+ ASK_MAX_LABEL = 120;
44624
+ ASK_MAX_DESCRIPTION = 240;
44625
+ ASK_MAX_OTHER = 400;
44368
44626
  MIRROR_ROLES = ["user", "assistant", "activity"];
44369
44627
  MIRROR_MAX_TEXT = 2 * 1024;
44370
44628
  MIRROR_MAX_FRAMES = 64;
44371
- CHAT_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
44629
+ CHAT_ID_RE2 = /^[a-zA-Z0-9_-]{1,128}$/;
44372
44630
  MAX_MESSAGE_LENGTH = 32 * 1024;
44373
44631
  MAX_BRIDGE_CLIENTS = 20;
44374
44632
  KEEPALIVE_MS2 = 25e3;
@@ -44381,6 +44639,13 @@ var init_api_bridge = __esm({
44381
44639
  // eviction can clearInterval too — the req 'close' handler may never fire on
44382
44640
  // an abrupt res.destroy(), which would otherwise leak the timer.
44383
44641
  keepalives = /* @__PURE__ */ new Map();
44642
+ // Per-client subscribed chat_id (null = no ?chat_id supplied = live-only legacy
44643
+ // consumer). Set when the client is added to this.clients in handleStream.
44644
+ // Server-side filtering in broadcast() uses this: each frame is only delivered
44645
+ // to the client(s) whose subscribed chat_id matches frame.chat_id. A null-
44646
+ // subscribed client receives nothing — the old "shared broadcast to all"
44647
+ // behaviour is intentionally replaced with per-client routing.
44648
+ clientChatId = /* @__PURE__ */ new Map();
44384
44649
  // chat_ids we have seen on an INBOUND POST. Outbound replies bind against this
44385
44650
  // set (consensus f5): the live session can only address a stream the dashboard
44386
44651
  // actually opened, never an arbitrary/forged id. Bounded + TTL'd so a long
@@ -44389,6 +44654,16 @@ var init_api_bridge = __esm({
44389
44654
  static MAX_KNOWN_CHAT_IDS = 64;
44390
44655
  static CHAT_ID_TTL_MS = 2 * 60 * 60 * 1e3;
44391
44656
  // 2h, mirror chat-session-store
44657
+ // ── Outstanding-question registry (gossip_ask round-trip) ───────────────────
44658
+ // qid → {chatId, questions, at}. An inbound answer is validated against the
44659
+ // EXACT set that was asked: the questionId must exist, each selected label must
44660
+ // be one of that question's option labels, `other` only when allowOther. The
44661
+ // registry is bounded + TTL'd (fail-closed eviction) so a long-running session
44662
+ // can't grow it unbounded and a stale qid can never be answered.
44663
+ outstandingQuestions = /* @__PURE__ */ new Map();
44664
+ static MAX_OUTSTANDING_QUESTIONS = 64;
44665
+ static QUESTION_TTL_MS = 2 * 60 * 60 * 1e3;
44666
+ // 2h, mirror knownChatIds
44392
44667
  // ── Mirror state (spec v2 §3) ──────────────────────────────────────────────
44393
44668
  // chat_ids eligible to RECEIVE mirror frames. SEPARATE from knownChatIds
44394
44669
  // (sonnet:f7/f12). P1#1: this set is seeded ONLY from the relay's validated
@@ -44398,19 +44673,6 @@ var init_api_bridge = __esm({
44398
44673
  // emitReply STILL gates on knownChatIds, so mirroring never widens the
44399
44674
  // outbound reply boundary (api-bridge.ts emitReply isKnownChatId check).
44400
44675
  mirrorChatIds = /* @__PURE__ */ new Map();
44401
- // The most-recently-registered active mirror chat_id (set in
44402
- // registerMirrorChatId, cleared/recomputed when that id is TTL-evicted).
44403
- // Used by emitMirror to route unresolved terminal frames to a live observer
44404
- // instead of the provisional buffer when one exists (fix: mirror frames
44405
- // previously fell into _provisional and were dropped by the dashboard client
44406
- // because bindSession never ran — the browser cannot send a session_id it
44407
- // doesn't know). SECURITY: only ever a chat_id already in mirrorChatIds.
44408
- // KNOWN LIMITATION (consensus 96350953-8ce94428): single-pointer, so with
44409
- // multiple concurrent dashboard tabs only the most-recently-registered one
44410
- // receives unresolved terminal frames; earlier tabs see no live activity until
44411
- // they send again. Accepted tradeoff — a strict improvement over the prior
44412
- // behaviour where the frames went to _provisional and EVERY tab saw nothing.
44413
- latestMirrorChatId = null;
44414
44676
  // Session→chat_id map (P1#5). The relay has no native "active session
44415
44677
  // chat_id" notion — a no-chat_id inbound POST mints a fresh UUID. We learn the
44416
44678
  // mapping from the dashboard turn: a turn carries BOTH a chat_id (validated)
@@ -44419,7 +44681,8 @@ var init_api_bridge = __esm({
44419
44681
  // through here. Established only from a validated dashboard inbound POST.
44420
44682
  sessionToChatId = /* @__PURE__ */ new Map();
44421
44683
  // Per-chat_id mirror rings (bounded FIFO + TTL + proactive sweep).
44422
- mirror = new MirrorEventStore();
44684
+ // Constructed with a FileChatStore when chatDir is provided, else NullChatStore.
44685
+ mirror;
44423
44686
  // PROVISIONAL buffer (P1#5 fallback): a purely-terminal mirror POST with no
44424
44687
  // resolvable chat_id (no body chat_id, no known session mapping) is buffered
44425
44688
  // under a provisional id so a later observer can backfill it — OR dropped,
@@ -44434,6 +44697,10 @@ var init_api_bridge = __esm({
44434
44697
  dropUnresolvedMirror = false;
44435
44698
  // session→chat_id entries share the same 2h TTL ceiling as knownChatIds.
44436
44699
  static MAX_SESSION_MAPPINGS = 64;
44700
+ constructor(opts = {}) {
44701
+ const store = opts.chatDir ? new FileChatStore(opts.chatDir) : new NullChatStore();
44702
+ this.mirror = new MirrorEventStore(MIRROR_RING_MAX, MIRROR_RING_TTL_MS, void 0, store);
44703
+ }
44437
44704
  /** Register (or clear) the in-process inbound sink. Called by RelayServer. */
44438
44705
  registerSink(fn) {
44439
44706
  this.sink = fn;
@@ -44523,6 +44790,165 @@ var init_api_bridge = __esm({
44523
44790
  this.broadcast({ type: "ack", chat_id: chatId, ts: (/* @__PURE__ */ new Date()).toISOString() });
44524
44791
  return true;
44525
44792
  }
44793
+ /**
44794
+ * OUTBOUND: the MCP `gossip_ask` tool asks the dashboard a selection question.
44795
+ * Gates on knownChatIds EXACTLY like emitReply — a question may only target a
44796
+ * stream the dashboard actually opened, never an arbitrary/forged id. On
44797
+ * success the validated question set is registered under `qid` (so a later
44798
+ * inbound answer can be validated against what was asked) and a `question`
44799
+ * frame is fanned out over SSE.
44800
+ *
44801
+ * LIVE-ONLY: like reply/ack, a `question` frame has NO per-chat_id ring — an
44802
+ * unanswered question is LOST on a dashboard reload (acceptable v1). The
44803
+ * registry entry survives a reload (so an answer typed after reconnect would
44804
+ * still validate), but the rendered card does not.
44805
+ *
44806
+ * Returns false (no registration, no broadcast) when the chat_id is
44807
+ * malformed/unbound or no SSE client is connected, so the MCP tool can no-op
44808
+ * honestly — same posture as emitReply.
44809
+ */
44810
+ emitQuestion(rawChatId, qid, questions) {
44811
+ const chatId = validateChatId(rawChatId);
44812
+ if (chatId === null) return false;
44813
+ if (!this.isKnownChatId(chatId)) return false;
44814
+ if (this.clients.size === 0) return false;
44815
+ if (typeof qid !== "string" || qid.length === 0) return false;
44816
+ if (!Array.isArray(questions) || questions.length === 0) return false;
44817
+ this.registerOutstandingQuestion(qid, chatId, questions);
44818
+ this.broadcast({ type: "question", chat_id: chatId, qid, questions, ts: (/* @__PURE__ */ new Date()).toISOString() });
44819
+ return true;
44820
+ }
44821
+ /**
44822
+ * INBOUND ANSWER (UNTRUSTED — this is the security boundary). The dashboard
44823
+ * POSTs `{chat_id, answer:{qid, responses:[{questionId, selected[], other?}]}}`.
44824
+ * VALIDATES fail-closed against the registered question set:
44825
+ * - chat_id is known (bound to an opened stream);
44826
+ * - qid is an outstanding question FOR THAT chat_id (cross-chat reuse → 400);
44827
+ * - each questionId exists in the asked set, with no duplicates / no missing;
44828
+ * - each `selected` label is one of THAT question's option labels (an unknown
44829
+ * label → 400; a single-select question accepts at most one label);
44830
+ * - `other` is only allowed when that question had allowOther, and is trimmed
44831
+ * + length-capped.
44832
+ * On VALID: a concise channel turn is formatted and delivered to the live CC
44833
+ * session via the SAME sink path inbound chat messages use (so it arrives as a
44834
+ * normal turn), then the qid is deleted (single-use). Returns a route-shaped
44835
+ * {status,payload}: 202 on accept, 400 on any validation failure, 503 when no
44836
+ * sink is wired.
44837
+ */
44838
+ handleAnswer(body) {
44839
+ const b = body ?? {};
44840
+ const chatId = validateChatId(b.chat_id);
44841
+ if (chatId === null) {
44842
+ return { status: 400, payload: { error: "invalid chat_id" } };
44843
+ }
44844
+ if (!this.isKnownChatId(chatId)) {
44845
+ return { status: 400, payload: { error: "unknown chat_id (not an open dashboard stream)" } };
44846
+ }
44847
+ const answer = b.answer;
44848
+ if (!answer || typeof answer !== "object") {
44849
+ return { status: 400, payload: { error: "answer must be an object" } };
44850
+ }
44851
+ const qid = answer.qid;
44852
+ if (typeof qid !== "string" || qid.length === 0) {
44853
+ return { status: 400, payload: { error: "answer.qid must be a non-empty string" } };
44854
+ }
44855
+ this.evictOutstandingQuestions(Date.now());
44856
+ const outstanding = this.outstandingQuestions.get(qid);
44857
+ if (!outstanding) {
44858
+ return { status: 400, payload: { error: "unknown or expired qid" } };
44859
+ }
44860
+ if (outstanding.chatId !== chatId) {
44861
+ return { status: 400, payload: { error: "qid does not belong to this chat_id" } };
44862
+ }
44863
+ const responses = answer.responses;
44864
+ if (!Array.isArray(responses) || responses.length === 0) {
44865
+ return { status: 400, payload: { error: "answer.responses must be a non-empty array" } };
44866
+ }
44867
+ if (responses.length > outstanding.questions.length) {
44868
+ return { status: 400, payload: { error: "too many responses" } };
44869
+ }
44870
+ const seen = /* @__PURE__ */ new Set();
44871
+ const parts = [];
44872
+ for (const r of responses) {
44873
+ if (!r || typeof r !== "object") {
44874
+ return { status: 400, payload: { error: "each response must be an object" } };
44875
+ }
44876
+ const rr = r;
44877
+ if (typeof rr.questionId !== "string") {
44878
+ return { status: 400, payload: { error: "response.questionId must be a string" } };
44879
+ }
44880
+ const questionId = rr.questionId;
44881
+ if (seen.has(questionId)) {
44882
+ return { status: 400, payload: { error: "duplicate questionId in responses" } };
44883
+ }
44884
+ const q = outstanding.questions.find((x) => x.questionId === questionId);
44885
+ if (!q) {
44886
+ return { status: 400, payload: { error: `unknown questionId "${questionId}"` } };
44887
+ }
44888
+ seen.add(questionId);
44889
+ if (!Array.isArray(rr.selected)) {
44890
+ return { status: 400, payload: { error: "response.selected must be an array" } };
44891
+ }
44892
+ const selected = rr.selected;
44893
+ if (!q.multiSelect && selected.length > 1) {
44894
+ return { status: 400, payload: { error: `question "${questionId}" is single-select` } };
44895
+ }
44896
+ if (selected.length > q.options.length) {
44897
+ return { status: 400, payload: { error: "too many selected labels" } };
44898
+ }
44899
+ const labels = new Set(q.options.map((o) => o.label));
44900
+ const chosen = [];
44901
+ const seenLabels = /* @__PURE__ */ new Set();
44902
+ for (const s of selected) {
44903
+ if (typeof s !== "string" || !labels.has(s)) {
44904
+ return { status: 400, payload: { error: `unknown option label for question "${questionId}"` } };
44905
+ }
44906
+ if (seenLabels.has(s)) {
44907
+ return { status: 400, payload: { error: "duplicate selected label" } };
44908
+ }
44909
+ seenLabels.add(s);
44910
+ chosen.push(s);
44911
+ }
44912
+ let otherText = null;
44913
+ if (rr.other !== void 0 && rr.other !== null) {
44914
+ if (!q.allowOther) {
44915
+ return { status: 400, payload: { error: `question "${questionId}" does not allow other` } };
44916
+ }
44917
+ if (typeof rr.other !== "string") {
44918
+ return { status: 400, payload: { error: "response.other must be a string" } };
44919
+ }
44920
+ const sanitized = rr.other.replace(/[\x00-\x1F\x7F]/g, " ").replace(/\s+/g, " ").replace(/\[answer/gi, "(answer").trim();
44921
+ if (sanitized.length > ASK_MAX_OTHER) {
44922
+ return { status: 400, payload: { error: "other text too long" } };
44923
+ }
44924
+ if (sanitized.length > 0) {
44925
+ otherText = sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
44926
+ }
44927
+ }
44928
+ if (chosen.length === 0 && otherText === null) {
44929
+ return { status: 400, payload: { error: `question "${questionId}" has no selection` } };
44930
+ }
44931
+ const segs = [];
44932
+ if (chosen.length > 0) segs.push(chosen.join(", "));
44933
+ if (otherText !== null) segs.push(`other: "${otherText}"`);
44934
+ parts.push(`${q.header}: ${segs.join(" \xB7 ")}`);
44935
+ }
44936
+ if (!this.sink) {
44937
+ return { status: 503, payload: { error: "No live Claude Code bridge session is active" } };
44938
+ }
44939
+ const turn = `[answer qid=${qid}] ${parts.join(" \xB7 ")}`;
44940
+ let delivered;
44941
+ try {
44942
+ delivered = this.sink(chatId, turn);
44943
+ } catch {
44944
+ delivered = false;
44945
+ }
44946
+ if (!delivered) {
44947
+ return { status: 503, payload: { error: "Bridge sink rejected the answer" } };
44948
+ }
44949
+ this.outstandingQuestions.delete(qid);
44950
+ return { status: 202, payload: { ok: true, qid, chat_id: chatId } };
44951
+ }
44526
44952
  /**
44527
44953
  * MIRROR ingress (spec v2 §3). Validates the (already body-capped + role-
44528
44954
  * checked-at-route) batch a SECOND time at the trust boundary, resolves the
@@ -44594,10 +45020,25 @@ var init_api_bridge = __esm({
44594
45020
  chatId = resolved;
44595
45021
  } else if (this.dropUnresolvedMirror) {
44596
45022
  return { status: 202, payload: { ok: true, chat_id: null, dropped: validated.length } };
44597
- } else if (this.latestMirrorChatId !== null && this.isMirrorChatId(this.latestMirrorChatId)) {
44598
- chatId = this.latestMirrorChatId;
44599
45023
  } else {
44600
- chatId = _BridgeHub.PROVISIONAL_CHAT_ID;
45024
+ this.evictMirrorChatIds(Date.now());
45025
+ const liveIds = Array.from(this.mirrorChatIds.keys());
45026
+ if (liveIds.length > 0) {
45027
+ let totalFrames = 0;
45028
+ for (const id of liveIds) {
45029
+ for (const { role, text } of validated) {
45030
+ const frame = this.mirror.push(id, role, text);
45031
+ this.broadcast(frame);
45032
+ totalFrames++;
45033
+ }
45034
+ }
45035
+ return {
45036
+ status: 202,
45037
+ payload: { ok: true, chat_id: null, fanout: liveIds.length, frames: totalFrames }
45038
+ };
45039
+ } else {
45040
+ chatId = _BridgeHub.PROVISIONAL_CHAT_ID;
45041
+ }
44601
45042
  }
44602
45043
  }
44603
45044
  const stamped = [];
@@ -44667,6 +45108,7 @@ var init_api_bridge = __esm({
44667
45108
  }
44668
45109
  this.backpressure.set(res, 0);
44669
45110
  this.clients.add(res);
45111
+ this.clientChatId.set(res, chatId);
44670
45112
  const keepalive = setInterval(() => {
44671
45113
  if (chatId !== null) this.touchMirrorChatId(chatId);
44672
45114
  try {
@@ -44680,6 +45122,7 @@ var init_api_bridge = __esm({
44680
45122
  req.on("close", () => {
44681
45123
  this.clearKeepalive(res);
44682
45124
  this.clients.delete(res);
45125
+ this.clientChatId.delete(res);
44683
45126
  });
44684
45127
  }
44685
45128
  /**
@@ -44718,7 +45161,16 @@ var init_api_bridge = __esm({
44718
45161
  return false;
44719
45162
  }
44720
45163
  }
44721
- /** Fan out one frame to every connected SSE client, with backpressure eviction. */
45164
+ /**
45165
+ * Fan out one frame to matching connected SSE clients (server-side filter),
45166
+ * with backpressure eviction.
45167
+ *
45168
+ * Each frame carries a chat_id. Only the client(s) subscribed to that
45169
+ * chat_id receive the write — clients subscribed to a different chat_id or
45170
+ * with a null subscription (legacy live-only consumer) are skipped. This
45171
+ * eliminates the prior behaviour of broadcasting EVERY frame to ALL clients
45172
+ * (the bandwidth/confidentiality gap noted in the multi-tab fix spec).
45173
+ */
44722
45174
  broadcast(frame) {
44723
45175
  const idLine = "id" in frame && typeof frame.id === "number" ? `id: ${frame.id}
44724
45176
  ` : "";
@@ -44726,6 +45178,8 @@ var init_api_bridge = __esm({
44726
45178
 
44727
45179
  `;
44728
45180
  for (const res of this.clients) {
45181
+ const subscribedChatId = this.clientChatId.get(res) ?? null;
45182
+ if (subscribedChatId === null || subscribedChatId !== frame.chat_id) continue;
44729
45183
  try {
44730
45184
  const ok = res.write(data);
44731
45185
  if (ok) {
@@ -44737,11 +45191,13 @@ var init_api_bridge = __esm({
44737
45191
  this.clearKeepalive(res);
44738
45192
  res.destroy();
44739
45193
  this.clients.delete(res);
45194
+ this.clientChatId.delete(res);
44740
45195
  }
44741
45196
  }
44742
45197
  } catch {
44743
45198
  this.clearKeepalive(res);
44744
45199
  this.clients.delete(res);
45200
+ this.clientChatId.delete(res);
44745
45201
  }
44746
45202
  }
44747
45203
  }
@@ -44790,6 +45246,34 @@ var init_api_bridge = __esm({
44790
45246
  if (now - v > _BridgeHub.CHAT_ID_TTL_MS) this.knownChatIds.delete(k);
44791
45247
  }
44792
45248
  }
45249
+ // ── Outstanding-question registry helpers (gossip_ask) ──────────────────────
45250
+ /**
45251
+ * Register a validated question set under `qid`. Bounded + TTL'd: a stale
45252
+ * entry is reaped before insert, and at capacity the oldest entry is dropped
45253
+ * to make room (fail-closed — an unanswered old question is forgotten rather
45254
+ * than letting the map grow unbounded).
45255
+ */
45256
+ registerOutstandingQuestion(qid, chatId, questions) {
45257
+ const now = Date.now();
45258
+ this.evictOutstandingQuestions(now);
45259
+ if (!this.outstandingQuestions.has(qid) && this.outstandingQuestions.size >= _BridgeHub.MAX_OUTSTANDING_QUESTIONS) {
45260
+ let oldestKey = null;
45261
+ let oldest = Infinity;
45262
+ for (const [k, v] of this.outstandingQuestions) {
45263
+ if (v.at < oldest) {
45264
+ oldest = v.at;
45265
+ oldestKey = k;
45266
+ }
45267
+ }
45268
+ if (oldestKey !== null) this.outstandingQuestions.delete(oldestKey);
45269
+ }
45270
+ this.outstandingQuestions.set(qid, { chatId, questions, at: now });
45271
+ }
45272
+ evictOutstandingQuestions(now) {
45273
+ for (const [k, v] of this.outstandingQuestions) {
45274
+ if (now - v.at > _BridgeHub.QUESTION_TTL_MS) this.outstandingQuestions.delete(k);
45275
+ }
45276
+ }
44793
45277
  // ── Mirror chat-id registry (SEPARATE from knownChatIds — P1#1/sonnet:f7) ──
44794
45278
  // Same bounded + TTL eviction discipline as knownChatIds, but a DISTINCT map:
44795
45279
  // mirrorChatIds gates emitMirror; knownChatIds gates emitReply. Keeping them
@@ -44816,7 +45300,6 @@ var init_api_bridge = __esm({
44816
45300
  if (oldestKey !== null) this.mirrorChatIds.delete(oldestKey);
44817
45301
  }
44818
45302
  this.mirrorChatIds.set(chatId, now);
44819
- this.latestMirrorChatId = chatId;
44820
45303
  if (isNew) this.backfillProvisional(chatId, now);
44821
45304
  }
44822
45305
  /**
@@ -44837,23 +45320,10 @@ var init_api_bridge = __esm({
44837
45320
  return this.mirrorChatIds.has(chatId);
44838
45321
  }
44839
45322
  evictMirrorChatIds(now) {
44840
- let evictedLatest = false;
44841
45323
  for (const [k, v] of this.mirrorChatIds) {
44842
45324
  if (now - v > _BridgeHub.CHAT_ID_TTL_MS) {
44843
45325
  this.mirrorChatIds.delete(k);
44844
- if (k === this.latestMirrorChatId) evictedLatest = true;
44845
- }
44846
- }
44847
- if (evictedLatest) {
44848
- let newestKey = null;
44849
- let newest = -Infinity;
44850
- for (const [k, v] of this.mirrorChatIds) {
44851
- if (v > newest) {
44852
- newest = v;
44853
- newestKey = k;
44854
- }
44855
45326
  }
44856
- this.latestMirrorChatId = newestKey;
44857
45327
  }
44858
45328
  }
44859
45329
  /**
@@ -45082,15 +45552,15 @@ function normalizeLegacyDegradedFields(report) {
45082
45552
  }
45083
45553
  function resolveDashboardRoot(projectRoot) {
45084
45554
  const candidates = [
45085
- (0, import_path78.resolve)(__dirname, "..", "dist-dashboard"),
45555
+ (0, import_path79.resolve)(__dirname, "..", "dist-dashboard"),
45086
45556
  // bundled: dist-mcp/mcp-server.js → ../dist-dashboard
45087
- (0, import_path78.resolve)(__dirname, "..", "..", "..", "..", "dist-dashboard"),
45557
+ (0, import_path79.resolve)(__dirname, "..", "..", "..", "..", "dist-dashboard"),
45088
45558
  // tsc dev: packages/relay/dist/dashboard → repo-root
45089
- (0, import_path78.join)(projectRoot, "dist-dashboard")
45559
+ (0, import_path79.join)(projectRoot, "dist-dashboard")
45090
45560
  // legacy dev fallback (git-clone running from repo root)
45091
45561
  ];
45092
45562
  for (const p of candidates) {
45093
- if ((0, import_fs70.existsSync)(p)) return p;
45563
+ if ((0, import_fs71.existsSync)(p)) return p;
45094
45564
  }
45095
45565
  return null;
45096
45566
  }
@@ -45117,7 +45587,7 @@ function readBody(req, maxBytes = MAX_BODY_SIZE) {
45117
45587
  });
45118
45588
  });
45119
45589
  }
45120
- var import_fs70, import_path78, import_crypto28, AUTH_MAX_ATTEMPTS, AUTH_LOCKOUT_MS, AUTH_ATTEMPT_TTL_MS, AUTH_ATTEMPTS_HARD_CAP, CHAT_MAX_BODY, BRIDGE_MAX_BODY, MIRROR_MAX_BODY, MIRROR_MIN_INTERVAL_MS, MIRROR_LAST_TURN_HARD_CAP, CHAT_MIN_INTERVAL_MS, DashboardRouter, MAX_BODY_SIZE;
45590
+ var import_fs71, import_path79, import_crypto28, AUTH_MAX_ATTEMPTS, AUTH_LOCKOUT_MS, AUTH_ATTEMPT_TTL_MS, AUTH_ATTEMPTS_HARD_CAP, CHAT_MAX_BODY, BRIDGE_MAX_BODY, MIRROR_MAX_BODY, MIRROR_MIN_INTERVAL_MS, MIRROR_LAST_TURN_HARD_CAP, CHAT_MIN_INTERVAL_MS, DashboardRouter, MAX_BODY_SIZE;
45121
45591
  var init_routes = __esm({
45122
45592
  "packages/relay/src/dashboard/routes.ts"() {
45123
45593
  "use strict";
@@ -45145,8 +45615,8 @@ var init_routes = __esm({
45145
45615
  init_api_bridge();
45146
45616
  init_chat_session_store();
45147
45617
  init_src4();
45148
- import_fs70 = require("fs");
45149
- import_path78 = require("path");
45618
+ import_fs71 = require("fs");
45619
+ import_path79 = require("path");
45150
45620
  import_crypto28 = require("crypto");
45151
45621
  AUTH_MAX_ATTEMPTS = 10;
45152
45622
  AUTH_LOCKOUT_MS = 6e4;
@@ -45164,6 +45634,7 @@ var init_routes = __esm({
45164
45634
  this.projectRoot = projectRoot;
45165
45635
  this.ctx = ctx2;
45166
45636
  this.dashboardRoot = resolveDashboardRoot(projectRoot);
45637
+ this.bridge = new BridgeHub({ chatDir: (0, import_path79.join)(projectRoot, ".gossip", "chat") });
45167
45638
  }
45168
45639
  auth;
45169
45640
  projectRoot;
@@ -45185,7 +45656,9 @@ var init_routes = __esm({
45185
45656
  // Dashboard ⇄ live-CC bridge transport (P1). Owns the outbound SSE client set
45186
45657
  // and the in-process inbound sink the MCP server registers. Distinct from the
45187
45658
  // dormant chatbot (consensus f14 — no dual-brain; /api/chat stays dormant).
45188
- bridge = new BridgeHub();
45659
+ // Constructed in the constructor body (not as a field initializer) so it can
45660
+ // receive the chatDir option derived from projectRoot.
45661
+ bridge;
45189
45662
  /**
45190
45663
  * Inject (or clear) the read-only chatbot agent. Called by the app layer
45191
45664
  * after boot via RelayServer.setChatbot. Passing null disables chat with a
@@ -45213,6 +45686,16 @@ var init_routes = __esm({
45213
45686
  emitBridgeReply(chatId, text) {
45214
45687
  return this.bridge.emitReply(chatId, text);
45215
45688
  }
45689
+ /**
45690
+ * Ask the dashboard a structured selection question (MCP `gossip_ask` tool).
45691
+ * The hub gates chat_id on knownChatIds exactly like emitReply, registers the
45692
+ * validated question set under `qid`, and fans out a `question` frame. Returns
45693
+ * false when the id was unbound/malformed or no SSE client is connected so the
45694
+ * caller can no-op honestly.
45695
+ */
45696
+ emitBridgeQuestion(chatId, qid, questions) {
45697
+ return this.bridge.emitQuestion(chatId, qid, questions);
45698
+ }
45216
45699
  /** Update live context (call when agents connect/disconnect) */
45217
45700
  updateContext(ctx2) {
45218
45701
  if (ctx2.agentConfigs !== void 0) this.ctx.agentConfigs = ctx2.agentConfigs;
@@ -45609,7 +46092,8 @@ var init_routes = __esm({
45609
46092
  this.json(res, 400, { error: "Invalid JSON body" });
45610
46093
  return true;
45611
46094
  }
45612
- const { status, payload } = this.bridge.handlePost(body);
46095
+ const hasAnswer = body !== null && typeof body === "object" && "answer" in body;
46096
+ const { status, payload } = hasAnswer ? this.bridge.handleAnswer(body) : this.bridge.handlePost(body);
45613
46097
  this.json(res, status, payload);
45614
46098
  return true;
45615
46099
  }
@@ -45663,14 +46147,17 @@ var init_routes = __esm({
45663
46147
  res.end("Dashboard assets not found. Reinstall gossipcat or rebuild from source.");
45664
46148
  return true;
45665
46149
  }
45666
- const htmlPath = (0, import_path78.join)(this.dashboardRoot, "index.html");
45667
- if (!(0, import_fs70.existsSync)(htmlPath)) {
46150
+ const htmlPath = (0, import_path79.join)(this.dashboardRoot, "index.html");
46151
+ if (!(0, import_fs71.existsSync)(htmlPath)) {
45668
46152
  res.writeHead(503, { "Content-Type": "text/plain" });
45669
46153
  res.end(`Dashboard index.html missing at ${this.dashboardRoot}. Reinstall gossipcat.`);
45670
46154
  return true;
45671
46155
  }
45672
- const html = (0, import_fs70.readFileSync)(htmlPath, "utf-8");
45673
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
46156
+ const html = (0, import_fs71.readFileSync)(htmlPath, "utf-8");
46157
+ res.writeHead(200, {
46158
+ "Content-Type": "text/html; charset=utf-8",
46159
+ "Cache-Control": "no-cache"
46160
+ });
45674
46161
  res.end(html);
45675
46162
  return true;
45676
46163
  }
@@ -45695,16 +46182,16 @@ var init_routes = __esm({
45695
46182
  const ext = "." + (relativePath.split(".").pop() || "");
45696
46183
  const mime = MIME[ext];
45697
46184
  if (!mime) return false;
45698
- const filePath = (0, import_path78.join)(this.dashboardRoot, relativePath);
46185
+ const filePath = (0, import_path79.join)(this.dashboardRoot, relativePath);
45699
46186
  try {
45700
- const realFile = (0, import_fs70.realpathSync)(filePath);
45701
- const realBase = (0, import_fs70.realpathSync)(this.dashboardRoot);
46187
+ const realFile = (0, import_fs71.realpathSync)(filePath);
46188
+ const realBase = (0, import_fs71.realpathSync)(this.dashboardRoot);
45702
46189
  if (!realFile.startsWith(realBase + "/")) {
45703
46190
  res.writeHead(404);
45704
46191
  res.end();
45705
46192
  return true;
45706
46193
  }
45707
- const data = (0, import_fs70.readFileSync)(realFile);
46194
+ const data = (0, import_fs71.readFileSync)(realFile);
45708
46195
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=86400" });
45709
46196
  res.end(data);
45710
46197
  return true;
@@ -45719,8 +46206,8 @@ var init_routes = __esm({
45719
46206
  return match ? match[1] : null;
45720
46207
  }
45721
46208
  getConsensusReports(page = 1, pageSize = 5) {
45722
- const { readdirSync: readdirSync22, readFileSync: readFileSync68, existsSync: existsSync73 } = require("fs");
45723
- const reportsDir = (0, import_path78.join)(this.projectRoot, ".gossip", "consensus-reports");
46209
+ const { readdirSync: readdirSync22, readFileSync: readFileSync69, existsSync: existsSync73 } = require("fs");
46210
+ const reportsDir = (0, import_path79.join)(this.projectRoot, ".gossip", "consensus-reports");
45724
46211
  let retractedConsensusIds = [];
45725
46212
  let roundRetractions = [];
45726
46213
  try {
@@ -45735,8 +46222,8 @@ var init_routes = __esm({
45735
46222
  const { statSync: statSync36 } = require("fs");
45736
46223
  const allFiles = readdirSync22(reportsDir).filter((f) => f.endsWith(".json")).sort((a, b) => {
45737
46224
  try {
45738
- const aTime = statSync36((0, import_path78.join)(reportsDir, a)).mtimeMs;
45739
- const bTime = statSync36((0, import_path78.join)(reportsDir, b)).mtimeMs;
46225
+ const aTime = statSync36((0, import_path79.join)(reportsDir, a)).mtimeMs;
46226
+ const bTime = statSync36((0, import_path79.join)(reportsDir, b)).mtimeMs;
45740
46227
  return bTime - aTime;
45741
46228
  } catch {
45742
46229
  return 0;
@@ -45747,13 +46234,13 @@ var init_routes = __esm({
45747
46234
  const clampedPage = Math.max(page, 1);
45748
46235
  const start = (clampedPage - 1) * clampedPageSize;
45749
46236
  const files = allFiles.slice(start, start + clampedPageSize);
45750
- const realReportsDir = (0, import_fs70.realpathSync)(reportsDir);
46237
+ const realReportsDir = (0, import_fs71.realpathSync)(reportsDir);
45751
46238
  const reports = files.map((f) => {
45752
46239
  try {
45753
- const filePath = (0, import_path78.join)(reportsDir, f);
45754
- const realFile = (0, import_fs70.realpathSync)(filePath);
46240
+ const filePath = (0, import_path79.join)(reportsDir, f);
46241
+ const realFile = (0, import_fs71.realpathSync)(filePath);
45755
46242
  if (!realFile.startsWith(realReportsDir + "/")) return null;
45756
- return normalizeLegacyDegradedFields(JSON.parse(readFileSync68(realFile, "utf-8")));
46243
+ return normalizeLegacyDegradedFields(JSON.parse(readFileSync69(realFile, "utf-8")));
45757
46244
  } catch {
45758
46245
  return null;
45759
46246
  }
@@ -45764,29 +46251,29 @@ var init_routes = __esm({
45764
46251
  }
45765
46252
  }
45766
46253
  archiveFindings() {
45767
- const { readdirSync: readdirSync22, readFileSync: readFileSync68, renameSync: renameSync17, writeFileSync: writeFileSync37, mkdirSync: mkdirSync44, existsSync: existsSync73 } = require("fs");
45768
- const reportsDir = (0, import_path78.join)(this.projectRoot, ".gossip", "consensus-reports");
45769
- const archiveDir = (0, import_path78.join)(this.projectRoot, ".gossip", "consensus-reports-archive");
46254
+ const { readdirSync: readdirSync22, readFileSync: readFileSync69, renameSync: renameSync18, writeFileSync: writeFileSync38, mkdirSync: mkdirSync45, existsSync: existsSync73 } = require("fs");
46255
+ const reportsDir = (0, import_path79.join)(this.projectRoot, ".gossip", "consensus-reports");
46256
+ const archiveDir = (0, import_path79.join)(this.projectRoot, ".gossip", "consensus-reports-archive");
45770
46257
  let archived = 0;
45771
46258
  if (existsSync73(reportsDir)) {
45772
46259
  const files = readdirSync22(reportsDir).filter((f) => f.endsWith(".json")).sort().reverse();
45773
46260
  if (files.length > 5) {
45774
- mkdirSync44(archiveDir, { recursive: true });
46261
+ mkdirSync45(archiveDir, { recursive: true });
45775
46262
  const toArchive = files.slice(5);
45776
46263
  for (const f of toArchive) {
45777
46264
  try {
45778
- renameSync17((0, import_path78.join)(reportsDir, f), (0, import_path78.join)(archiveDir, f));
46265
+ renameSync18((0, import_path79.join)(reportsDir, f), (0, import_path79.join)(archiveDir, f));
45779
46266
  archived++;
45780
46267
  } catch {
45781
46268
  }
45782
46269
  }
45783
46270
  }
45784
46271
  }
45785
- const findingsPath = (0, import_path78.join)(this.projectRoot, ".gossip", "implementation-findings.jsonl");
46272
+ const findingsPath = (0, import_path79.join)(this.projectRoot, ".gossip", "implementation-findings.jsonl");
45786
46273
  let findingsCleared = 0;
45787
46274
  if (existsSync73(findingsPath)) {
45788
46275
  try {
45789
- const lines = readFileSync68(findingsPath, "utf-8").trim().split("\n").filter(Boolean);
46276
+ const lines = readFileSync69(findingsPath, "utf-8").trim().split("\n").filter(Boolean);
45790
46277
  const kept = lines.filter((line) => {
45791
46278
  try {
45792
46279
  const entry = JSON.parse(line);
@@ -45799,7 +46286,7 @@ var init_routes = __esm({
45799
46286
  return true;
45800
46287
  }
45801
46288
  });
45802
- writeFileSync37(findingsPath, kept.join("\n") + (kept.length > 0 ? "\n" : ""));
46289
+ writeFileSync38(findingsPath, kept.join("\n") + (kept.length > 0 ? "\n" : ""));
45803
46290
  } catch {
45804
46291
  }
45805
46292
  }
@@ -45816,14 +46303,14 @@ var init_routes = __esm({
45816
46303
  });
45817
46304
 
45818
46305
  // packages/relay/src/dashboard/ws.ts
45819
- var import_fs71, import_fs72, import_path79, DashboardWs;
46306
+ var import_fs72, import_fs73, import_path80, DashboardWs;
45820
46307
  var init_ws = __esm({
45821
46308
  "packages/relay/src/dashboard/ws.ts"() {
45822
46309
  "use strict";
45823
46310
  init_wrapper();
45824
- import_fs71 = require("fs");
45825
46311
  import_fs72 = require("fs");
45826
- import_path79 = require("path");
46312
+ import_fs73 = require("fs");
46313
+ import_path80 = require("path");
45827
46314
  DashboardWs = class {
45828
46315
  clients = /* @__PURE__ */ new Set();
45829
46316
  logWatcher = null;
@@ -45848,15 +46335,15 @@ var init_ws = __esm({
45848
46335
  /** Start watching mcp.log for new lines and broadcasting them to connected clients. */
45849
46336
  startLogWatcher(projectRoot) {
45850
46337
  this.stopLogWatcher();
45851
- this.logPath = (0, import_path79.join)(projectRoot, ".gossip", "mcp.log");
45852
- if (!(0, import_fs71.existsSync)(this.logPath)) return;
46338
+ this.logPath = (0, import_path80.join)(projectRoot, ".gossip", "mcp.log");
46339
+ if (!(0, import_fs72.existsSync)(this.logPath)) return;
45853
46340
  try {
45854
- this.logOffset = (0, import_fs71.statSync)(this.logPath).size;
46341
+ this.logOffset = (0, import_fs72.statSync)(this.logPath).size;
45855
46342
  } catch {
45856
46343
  this.logOffset = 0;
45857
46344
  }
45858
46345
  try {
45859
- this.logWatcher = (0, import_fs72.watch)(this.logPath, () => {
46346
+ this.logWatcher = (0, import_fs73.watch)(this.logPath, () => {
45860
46347
  if (this.clients.size === 0) return;
45861
46348
  this.readNewLines();
45862
46349
  });
@@ -45873,7 +46360,7 @@ var init_ws = __esm({
45873
46360
  if (this.logReading) return;
45874
46361
  this.logReading = true;
45875
46362
  try {
45876
- const currentSize = (0, import_fs71.statSync)(this.logPath).size;
46363
+ const currentSize = (0, import_fs72.statSync)(this.logPath).size;
45877
46364
  if (currentSize < this.logOffset) {
45878
46365
  this.logOffset = 0;
45879
46366
  this.logCarry = "";
@@ -45886,7 +46373,7 @@ var init_ws = __esm({
45886
46373
  const readFrom = capped ? (this.logCarry = "", this.logCapped = true, currentSize - 65536) : (this.logCapped = false, this.logOffset);
45887
46374
  let stream;
45888
46375
  try {
45889
- stream = (0, import_fs71.createReadStream)(this.logPath, { start: readFrom, end: currentSize - 1 });
46376
+ stream = (0, import_fs72.createReadStream)(this.logPath, { start: readFrom, end: currentSize - 1 });
45890
46377
  } catch {
45891
46378
  this.logReading = false;
45892
46379
  return;
@@ -46346,6 +46833,19 @@ var init_server = __esm({
46346
46833
  emitBridgeReply(chatId, text) {
46347
46834
  return this.dashboardRouter?.emitBridgeReply(chatId, text) ?? false;
46348
46835
  }
46836
+ /**
46837
+ * Ask the dashboard a structured selection question (MCP `gossip_ask` tool).
46838
+ * Mirrors emitBridgeReply: chat_id is re-validated + bound to a stream the
46839
+ * dashboard actually opened (the same knownChatIds gate as emitReply), the
46840
+ * validated question set is registered under `qid` so a later inbound answer
46841
+ * can be validated against what was asked, and a `question` frame fans out
46842
+ * over SSE. Returns false (no registration, no broadcast) when the dashboard
46843
+ * is disabled, the chat_id is unbound/malformed, or no SSE client is connected
46844
+ * — so the caller can no-op honestly rather than claim a delivery.
46845
+ */
46846
+ emitBridgeQuestion(chatId, qid, questions) {
46847
+ return this.dashboardRouter?.emitBridgeQuestion(chatId, qid, questions) ?? false;
46848
+ }
46349
46849
  };
46350
46850
  }
46351
46851
  });
@@ -46381,7 +46881,8 @@ __export(src_exports4, {
46381
46881
  emitDashboardEvent: () => emitDashboardEvent,
46382
46882
  hasMemoryQuery: () => hasMemoryQuery,
46383
46883
  recordMemoryQueryAttribution: () => recordMemoryQueryAttribution,
46384
- sweepExpiredAgents: () => sweepExpiredAgents
46884
+ sweepExpiredAgents: () => sweepExpiredAgents,
46885
+ validateAskQuestions: () => validateAskQuestions
46385
46886
  });
46386
46887
  var init_src5 = __esm({
46387
46888
  "packages/relay/src/index.ts"() {
@@ -46720,14 +47221,14 @@ __export(sandbox_exports, {
46720
47221
  });
46721
47222
  function rotateIfNeeded2(filePath, maxBytes) {
46722
47223
  try {
46723
- const st = (0, import_fs73.statSync)(filePath);
47224
+ const st = (0, import_fs74.statSync)(filePath);
46724
47225
  if (st.size < maxBytes) return;
46725
47226
  const rotated = filePath + ".1";
46726
47227
  try {
46727
- if ((0, import_fs73.existsSync)(rotated)) (0, import_fs73.unlinkSync)(rotated);
47228
+ if ((0, import_fs74.existsSync)(rotated)) (0, import_fs74.unlinkSync)(rotated);
46728
47229
  } catch {
46729
47230
  }
46730
- (0, import_fs73.renameSync)(filePath, rotated);
47231
+ (0, import_fs74.renameSync)(filePath, rotated);
46731
47232
  } catch {
46732
47233
  }
46733
47234
  }
@@ -46800,9 +47301,9 @@ Tests passing is NOT sufficient verification \u2014 the 2026-04-22 incident had
46800
47301
  }
46801
47302
  function readSandboxMode(projectRoot) {
46802
47303
  try {
46803
- const p = (0, import_path80.join)(projectRoot, ".gossip", "config.json");
46804
- if (!(0, import_fs73.existsSync)(p)) return "warn";
46805
- const raw = JSON.parse((0, import_fs73.readFileSync)(p, "utf-8"));
47304
+ const p = (0, import_path81.join)(projectRoot, ".gossip", "config.json");
47305
+ if (!(0, import_fs74.existsSync)(p)) return "warn";
47306
+ const raw = JSON.parse((0, import_fs74.readFileSync)(p, "utf-8"));
46806
47307
  const mode = raw?.sandboxEnforcement;
46807
47308
  if (mode === "off" || mode === "warn" || mode === "block") return mode;
46808
47309
  return "warn";
@@ -46812,8 +47313,8 @@ function readSandboxMode(projectRoot) {
46812
47313
  }
46813
47314
  function recordDispatchMetadata(projectRoot, meta3) {
46814
47315
  try {
46815
- const dir = (0, import_path80.join)(projectRoot, ".gossip");
46816
- (0, import_fs73.mkdirSync)(dir, { recursive: true });
47316
+ const dir = (0, import_path81.join)(projectRoot, ".gossip");
47317
+ (0, import_fs74.mkdirSync)(dir, { recursive: true });
46817
47318
  const snapshotted = { ...meta3 };
46818
47319
  if (meta3.writeMode === "scoped" || meta3.writeMode === "worktree") {
46819
47320
  try {
@@ -46831,15 +47332,15 @@ function recordDispatchMetadata(projectRoot, meta3) {
46831
47332
  } catch {
46832
47333
  }
46833
47334
  }
46834
- (0, import_fs73.appendFileSync)((0, import_path80.join)(dir, METADATA_FILE), JSON.stringify(snapshotted) + "\n");
47335
+ (0, import_fs74.appendFileSync)((0, import_path81.join)(dir, METADATA_FILE), JSON.stringify(snapshotted) + "\n");
46835
47336
  } catch {
46836
47337
  }
46837
47338
  }
46838
47339
  function updateDispatchMetadata(projectRoot, taskId, patch) {
46839
47340
  try {
46840
- const p = (0, import_path80.join)(projectRoot, ".gossip", METADATA_FILE);
46841
- if (!(0, import_fs73.existsSync)(p)) return false;
46842
- const raw = (0, import_fs73.readFileSync)(p, "utf-8");
47341
+ const p = (0, import_path81.join)(projectRoot, ".gossip", METADATA_FILE);
47342
+ if (!(0, import_fs74.existsSync)(p)) return false;
47343
+ const raw = (0, import_fs74.readFileSync)(p, "utf-8");
46843
47344
  const lines = raw.split("\n");
46844
47345
  let patched = false;
46845
47346
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -46857,7 +47358,7 @@ function updateDispatchMetadata(projectRoot, taskId, patch) {
46857
47358
  }
46858
47359
  }
46859
47360
  if (!patched) return false;
46860
- (0, import_fs73.writeFileSync)(p, lines.join("\n"));
47361
+ (0, import_fs74.writeFileSync)(p, lines.join("\n"));
46861
47362
  return true;
46862
47363
  } catch {
46863
47364
  return false;
@@ -46866,14 +47367,14 @@ function updateDispatchMetadata(projectRoot, taskId, patch) {
46866
47367
  function stampTaskSentinel(projectRoot, taskId) {
46867
47368
  if (!taskId) return null;
46868
47369
  try {
46869
- const dir = (0, import_path80.join)(projectRoot, ".gossip", SENTINEL_DIR);
46870
- (0, import_fs73.mkdirSync)(dir, { recursive: true });
47370
+ const dir = (0, import_path81.join)(projectRoot, ".gossip", SENTINEL_DIR);
47371
+ (0, import_fs74.mkdirSync)(dir, { recursive: true });
46871
47372
  const slug = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
46872
- const path7 = (0, import_path80.join)(dir, `${slug}.sentinel`);
46873
- const fd = (0, import_fs73.openSync)(path7, "w");
46874
- (0, import_fs73.closeSync)(fd);
47373
+ const path7 = (0, import_path81.join)(dir, `${slug}.sentinel`);
47374
+ const fd = (0, import_fs74.openSync)(path7, "w");
47375
+ (0, import_fs74.closeSync)(fd);
46875
47376
  const stampTime = new Date(Date.now() - 2e3);
46876
- (0, import_fs73.utimesSync)(path7, stampTime, stampTime);
47377
+ (0, import_fs74.utimesSync)(path7, stampTime, stampTime);
46877
47378
  return path7;
46878
47379
  } catch {
46879
47380
  return null;
@@ -46882,15 +47383,15 @@ function stampTaskSentinel(projectRoot, taskId) {
46882
47383
  function cleanupTaskSentinel(sentinelPath) {
46883
47384
  if (!sentinelPath) return;
46884
47385
  try {
46885
- (0, import_fs73.unlinkSync)(sentinelPath);
47386
+ (0, import_fs74.unlinkSync)(sentinelPath);
46886
47387
  } catch {
46887
47388
  }
46888
47389
  }
46889
47390
  function lookupDispatchMetadata(projectRoot, taskId) {
46890
47391
  try {
46891
- const p = (0, import_path80.join)(projectRoot, ".gossip", METADATA_FILE);
46892
- if (!(0, import_fs73.existsSync)(p)) return null;
46893
- const raw = (0, import_fs73.readFileSync)(p, "utf-8");
47392
+ const p = (0, import_path81.join)(projectRoot, ".gossip", METADATA_FILE);
47393
+ if (!(0, import_fs74.existsSync)(p)) return null;
47394
+ const raw = (0, import_fs74.readFileSync)(p, "utf-8");
46894
47395
  const lines = raw.split("\n").filter(Boolean);
46895
47396
  for (let i = lines.length - 1; i >= 0; i--) {
46896
47397
  try {
@@ -46922,21 +47423,21 @@ function parseGitStatus(porcelain) {
46922
47423
  function normalizeScope(scope, projectRoot) {
46923
47424
  let s = scope.trim();
46924
47425
  if (!s) return "";
46925
- if ((0, import_path80.isAbsolute)(s)) {
46926
- s = (0, import_path80.relative)(projectRoot, s);
47426
+ if ((0, import_path81.isAbsolute)(s)) {
47427
+ s = (0, import_path81.relative)(projectRoot, s);
46927
47428
  } else if (s.startsWith("./")) {
46928
47429
  s = s.slice(2);
46929
47430
  }
46930
- s = (0, import_path80.normalize)(s).replace(/\/+$/, "");
47431
+ s = (0, import_path81.normalize)(s).replace(/\/+$/, "");
46931
47432
  if (s === "." || s === "") return "";
46932
47433
  return s;
46933
47434
  }
46934
47435
  function isInsideScope(filePath, scope) {
46935
47436
  if (!scope) return true;
46936
- const f = (0, import_path80.normalize)(filePath).replace(/^\.\//, "");
47437
+ const f = (0, import_path81.normalize)(filePath).replace(/^\.\//, "");
46937
47438
  const s = scope.replace(/^\.\//, "").replace(/\/+$/, "");
46938
47439
  if (f === s) return true;
46939
- return f.startsWith(s + "/") || f.startsWith(s + import_path80.sep);
47440
+ return f.startsWith(s + "/") || f.startsWith(s + import_path81.sep);
46940
47441
  }
46941
47442
  function detectBoundaryEscapes(meta3, modifiedFiles, projectRoot) {
46942
47443
  const mode = meta3.writeMode;
@@ -46997,8 +47498,8 @@ function recordBoundaryEscape(projectRoot, meta3, violations, mode) {
46997
47498
  } catch {
46998
47499
  }
46999
47500
  try {
47000
- const dir = (0, import_path80.join)(projectRoot, ".gossip");
47001
- (0, import_fs73.mkdirSync)(dir, { recursive: true });
47501
+ const dir = (0, import_path81.join)(projectRoot, ".gossip");
47502
+ (0, import_fs74.mkdirSync)(dir, { recursive: true });
47002
47503
  const line = {
47003
47504
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
47004
47505
  taskId: meta3.taskId,
@@ -47009,15 +47510,15 @@ function recordBoundaryEscape(projectRoot, meta3, violations, mode) {
47009
47510
  action: mode
47010
47511
  // "warn" or "block"
47011
47512
  };
47012
- const escapeFile = (0, import_path80.join)(dir, BOUNDARY_ESCAPE_FILE);
47513
+ const escapeFile = (0, import_path81.join)(dir, BOUNDARY_ESCAPE_FILE);
47013
47514
  rotateIfNeeded2(escapeFile, MAX_BOUNDARY_ESCAPE_BYTES);
47014
- (0, import_fs73.appendFileSync)(escapeFile, JSON.stringify(line) + "\n");
47515
+ (0, import_fs74.appendFileSync)(escapeFile, JSON.stringify(line) + "\n");
47015
47516
  } catch {
47016
47517
  }
47017
47518
  }
47018
47519
  function canonicalize(p) {
47019
47520
  try {
47020
- return (0, import_path80.resolve)(p).replace(/\/+$/, "") || "/";
47521
+ return (0, import_path81.resolve)(p).replace(/\/+$/, "") || "/";
47021
47522
  } catch {
47022
47523
  return p.replace(/\/+$/, "") || "/";
47023
47524
  }
@@ -47128,7 +47629,7 @@ function buildAuditExclusions(projectRoot, ownWorktree, scope) {
47128
47629
  for (const v of expandTmpVariants(wt)) excl.add(v);
47129
47630
  }
47130
47631
  if (scope) {
47131
- const s = canonicalize((0, import_path80.join)(projectRoot, scope));
47632
+ const s = canonicalize((0, import_path81.join)(projectRoot, scope));
47132
47633
  for (const v of expandTmpVariants(s)) excl.add(v);
47133
47634
  }
47134
47635
  return Array.from(excl);
@@ -47217,12 +47718,12 @@ function auditFilesystemSinceSentinel(projectRoot, meta3, options = {}) {
47217
47718
  return { violations: [], skipped: "win32" };
47218
47719
  }
47219
47720
  const sentinel = meta3.sentinelPath;
47220
- if (!sentinel || !(0, import_fs73.existsSync)(sentinel)) {
47721
+ if (!sentinel || !(0, import_fs74.existsSync)(sentinel)) {
47221
47722
  return { violations: [], skipped: "sentinel missing" };
47222
47723
  }
47223
47724
  let sentinelMtimeMs = 0;
47224
47725
  try {
47225
- sentinelMtimeMs = (0, import_fs73.statSync)(sentinel).mtimeMs;
47726
+ sentinelMtimeMs = (0, import_fs74.statSync)(sentinel).mtimeMs;
47226
47727
  } catch {
47227
47728
  }
47228
47729
  if (sentinelMtimeMs === 0) {
@@ -47234,11 +47735,11 @@ function auditFilesystemSinceSentinel(projectRoot, meta3, options = {}) {
47234
47735
  );
47235
47736
  const exclusions = buildAuditExclusions(projectRoot, meta3.worktreePath, options.scope);
47236
47737
  const findBin = options.findBinary ?? "find";
47237
- const sentinelDir = canonicalize((0, import_path80.join)(projectRoot, ".gossip", SENTINEL_DIR));
47738
+ const sentinelDir = canonicalize((0, import_path81.join)(projectRoot, ".gossip", SENTINEL_DIR));
47238
47739
  const mainSet = /* @__PURE__ */ new Set();
47239
47740
  const sensitiveSet = /* @__PURE__ */ new Set();
47240
47741
  for (const root of scanRoots) {
47241
- if (!(0, import_fs73.existsSync)(root)) continue;
47742
+ if (!(0, import_fs74.existsSync)(root)) continue;
47242
47743
  const canonRoot = canonicalize(root);
47243
47744
  const allExcl = [...exclusions, ...expandTmpVariants(sentinelDir)];
47244
47745
  const args = buildFindPruneArgs(canonRoot, allExcl, sentinel);
@@ -47274,7 +47775,7 @@ function auditFilesystemSinceSentinel(projectRoot, meta3, options = {}) {
47274
47775
  }
47275
47776
  const sensitiveTargets = buildSensitiveTargets(platform2);
47276
47777
  for (const target of sensitiveTargets) {
47277
- if (!(0, import_fs73.existsSync)(target.path)) continue;
47778
+ if (!(0, import_fs74.existsSync)(target.path)) continue;
47278
47779
  const canonTarget = canonicalize(target.path);
47279
47780
  const args = buildSensitiveFindArgs(
47280
47781
  canonTarget,
@@ -47348,8 +47849,8 @@ function auditFilesystemSinceSentinel(projectRoot, meta3, options = {}) {
47348
47849
  }
47349
47850
  function recordLayer3Violations(projectRoot, meta3, violations, source) {
47350
47851
  try {
47351
- const dir = (0, import_path80.join)(projectRoot, ".gossip");
47352
- (0, import_fs73.mkdirSync)(dir, { recursive: true });
47852
+ const dir = (0, import_path81.join)(projectRoot, ".gossip");
47853
+ (0, import_fs74.mkdirSync)(dir, { recursive: true });
47353
47854
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
47354
47855
  const lines = violations.map(
47355
47856
  (path7) => JSON.stringify({
@@ -47360,9 +47861,9 @@ function recordLayer3Violations(projectRoot, meta3, violations, source) {
47360
47861
  source
47361
47862
  })
47362
47863
  );
47363
- const escapeFile = (0, import_path80.join)(dir, BOUNDARY_ESCAPE_FILE);
47864
+ const escapeFile = (0, import_path81.join)(dir, BOUNDARY_ESCAPE_FILE);
47364
47865
  rotateIfNeeded2(escapeFile, MAX_BOUNDARY_ESCAPE_BYTES);
47365
- (0, import_fs73.appendFileSync)(escapeFile, lines.join("\n") + "\n");
47866
+ (0, import_fs74.appendFileSync)(escapeFile, lines.join("\n") + "\n");
47366
47867
  } catch {
47367
47868
  }
47368
47869
  if (source === "layer3-sensitive" && violations.length > 0) {
@@ -47419,14 +47920,14 @@ function runLayer3Audit(projectRoot, taskId) {
47419
47920
  }
47420
47921
  return { blockError, warnPrefix };
47421
47922
  }
47422
- var import_child_process12, import_fs73, import_os5, import_path80, METADATA_FILE, BOUNDARY_ESCAPE_FILE, SENTINEL_DIR, MAX_BOUNDARY_ESCAPE_BYTES, MAX_PREMISE_VERIFICATION_BYTES, BOUNDARY_ALLOWLIST, SYSTEM_PREFIXES, SCOPE_NOTE, NUMERAL, TARGETS, MODIFIER, CLAIM_PATTERNS, UNVERIFIED_NOTE_SENTINEL, LAYER3_KEEP_TMPDIR_PREFIXES, __test__;
47923
+ var import_child_process12, import_fs74, import_os5, import_path81, METADATA_FILE, BOUNDARY_ESCAPE_FILE, SENTINEL_DIR, MAX_BOUNDARY_ESCAPE_BYTES, MAX_PREMISE_VERIFICATION_BYTES, BOUNDARY_ALLOWLIST, SYSTEM_PREFIXES, SCOPE_NOTE, NUMERAL, TARGETS, MODIFIER, CLAIM_PATTERNS, UNVERIFIED_NOTE_SENTINEL, LAYER3_KEEP_TMPDIR_PREFIXES, __test__;
47423
47924
  var init_sandbox2 = __esm({
47424
47925
  "apps/cli/src/sandbox.ts"() {
47425
47926
  "use strict";
47426
47927
  import_child_process12 = require("child_process");
47427
- import_fs73 = require("fs");
47928
+ import_fs74 = require("fs");
47428
47929
  import_os5 = require("os");
47429
- import_path80 = require("path");
47930
+ import_path81 = require("path");
47430
47931
  METADATA_FILE = "dispatch-metadata.jsonl";
47431
47932
  BOUNDARY_ESCAPE_FILE = "boundary-escapes.jsonl";
47432
47933
  SENTINEL_DIR = "sentinels";
@@ -47498,7 +47999,7 @@ __export(dispatch_prompt_storage_exports, {
47498
47999
  writeDispatchPrompt: () => writeDispatchPrompt
47499
48000
  });
47500
48001
  function dispatchPromptsDir(projectRoot) {
47501
- return (0, import_path81.join)(projectRoot, ".gossip", DISPATCH_PROMPTS_SUBDIR);
48002
+ return (0, import_path82.join)(projectRoot, ".gossip", DISPATCH_PROMPTS_SUBDIR);
47502
48003
  }
47503
48004
  function assertSafeTaskId(taskId) {
47504
48005
  if (typeof taskId !== "string" || taskId.length === 0) {
@@ -47511,35 +48012,35 @@ function assertSafeTaskId(taskId) {
47511
48012
  function writeDispatchPrompt(projectRoot, taskId, body) {
47512
48013
  assertSafeTaskId(taskId);
47513
48014
  const dir = dispatchPromptsDir(projectRoot);
47514
- (0, import_fs74.mkdirSync)(dir, { recursive: true });
47515
- const finalPath = (0, import_path81.join)(dir, `${taskId}.txt`);
47516
- const tmpPath = (0, import_path81.join)(dir, `${taskId}.txt.${(0, import_crypto30.randomUUID)().slice(0, 8)}.tmp`);
48015
+ (0, import_fs75.mkdirSync)(dir, { recursive: true });
48016
+ const finalPath = (0, import_path82.join)(dir, `${taskId}.txt`);
48017
+ const tmpPath = (0, import_path82.join)(dir, `${taskId}.txt.${(0, import_crypto30.randomUUID)().slice(0, 8)}.tmp`);
47517
48018
  try {
47518
- (0, import_fs74.writeFileSync)(tmpPath, body, "utf8");
47519
- (0, import_fs74.renameSync)(tmpPath, finalPath);
48019
+ (0, import_fs75.writeFileSync)(tmpPath, body, "utf8");
48020
+ (0, import_fs75.renameSync)(tmpPath, finalPath);
47520
48021
  } catch (err) {
47521
48022
  try {
47522
- (0, import_fs74.unlinkSync)(tmpPath);
48023
+ (0, import_fs75.unlinkSync)(tmpPath);
47523
48024
  } catch {
47524
48025
  }
47525
48026
  throw err;
47526
48027
  }
47527
- return (0, import_path81.resolve)(finalPath);
48028
+ return (0, import_path82.resolve)(finalPath);
47528
48029
  }
47529
48030
  function cleanupExpiredDispatchPrompts(projectRoot, maxAgeMs = DEFAULT_PROMPT_TTL_MS, capBytes = DISPATCH_PROMPT_CAP_BYTES) {
47530
48031
  const dir = dispatchPromptsDir(projectRoot);
47531
- if (!(0, import_fs74.existsSync)(dir)) return { evictedAge: 0, evictedCap: 0 };
48032
+ if (!(0, import_fs75.existsSync)(dir)) return { evictedAge: 0, evictedCap: 0 };
47532
48033
  const now = Date.now();
47533
48034
  let entries = [];
47534
48035
  try {
47535
- for (const name of (0, import_fs74.readdirSync)(dir)) {
47536
- const path7 = (0, import_path81.join)(dir, name);
48036
+ for (const name of (0, import_fs75.readdirSync)(dir)) {
48037
+ const path7 = (0, import_path82.join)(dir, name);
47537
48038
  try {
47538
- const st = (0, import_fs74.statSync)(path7);
48039
+ const st = (0, import_fs75.statSync)(path7);
47539
48040
  if (!st.isFile()) continue;
47540
48041
  entries.push({ name, path: path7, mtimeMs: st.mtimeMs, size: st.size });
47541
48042
  } catch (err) {
47542
- process.stderr.write(`[gossipcat] dispatch-prompt stat failed for ${(0, import_path81.basename)(path7)}: ${err.message}
48043
+ process.stderr.write(`[gossipcat] dispatch-prompt stat failed for ${(0, import_path82.basename)(path7)}: ${err.message}
47543
48044
  `);
47544
48045
  }
47545
48046
  }
@@ -47553,7 +48054,7 @@ function cleanupExpiredDispatchPrompts(projectRoot, maxAgeMs = DEFAULT_PROMPT_TT
47553
48054
  for (const e of entries) {
47554
48055
  if (now - e.mtimeMs > maxAgeMs) {
47555
48056
  try {
47556
- (0, import_fs74.unlinkSync)(e.path);
48057
+ (0, import_fs75.unlinkSync)(e.path);
47557
48058
  evictedAge++;
47558
48059
  } catch (err) {
47559
48060
  process.stderr.write(`[gossipcat] dispatch-prompt unlink failed for ${e.name}: ${err.message}
@@ -47570,7 +48071,7 @@ function cleanupExpiredDispatchPrompts(projectRoot, maxAgeMs = DEFAULT_PROMPT_TT
47570
48071
  for (const e of survivors) {
47571
48072
  if (totalBytes <= capBytes) break;
47572
48073
  try {
47573
- (0, import_fs74.unlinkSync)(e.path);
48074
+ (0, import_fs75.unlinkSync)(e.path);
47574
48075
  totalBytes -= e.size;
47575
48076
  evictedCap++;
47576
48077
  } catch (err) {
@@ -47583,18 +48084,18 @@ function cleanupExpiredDispatchPrompts(projectRoot, maxAgeMs = DEFAULT_PROMPT_TT
47583
48084
  }
47584
48085
  function pruneOrphanDispatchPrompts(projectRoot, knownTaskIds, maxAgeMs = DEFAULT_PROMPT_TTL_MS) {
47585
48086
  const dir = dispatchPromptsDir(projectRoot);
47586
- if (!(0, import_fs74.existsSync)(dir)) return { orphans: 0, aged: 0 };
48087
+ if (!(0, import_fs75.existsSync)(dir)) return { orphans: 0, aged: 0 };
47587
48088
  const now = Date.now();
47588
48089
  let orphans = 0;
47589
48090
  let aged = 0;
47590
48091
  try {
47591
- for (const name of (0, import_fs74.readdirSync)(dir)) {
48092
+ for (const name of (0, import_fs75.readdirSync)(dir)) {
47592
48093
  if (!name.endsWith(".txt")) continue;
47593
48094
  const taskId = name.slice(0, -4);
47594
- const path7 = (0, import_path81.join)(dir, name);
48095
+ const path7 = (0, import_path82.join)(dir, name);
47595
48096
  let mtimeMs = 0;
47596
48097
  try {
47597
- mtimeMs = (0, import_fs74.statSync)(path7).mtimeMs;
48098
+ mtimeMs = (0, import_fs75.statSync)(path7).mtimeMs;
47598
48099
  } catch {
47599
48100
  continue;
47600
48101
  }
@@ -47602,7 +48103,7 @@ function pruneOrphanDispatchPrompts(projectRoot, knownTaskIds, maxAgeMs = DEFAUL
47602
48103
  const orphaned = !knownTaskIds.has(taskId);
47603
48104
  if (tooOld || orphaned) {
47604
48105
  try {
47605
- (0, import_fs74.unlinkSync)(path7);
48106
+ (0, import_fs75.unlinkSync)(path7);
47606
48107
  if (orphaned) orphans++;
47607
48108
  if (tooOld) aged++;
47608
48109
  } catch (err) {
@@ -47619,14 +48120,14 @@ function pruneOrphanDispatchPrompts(projectRoot, knownTaskIds, maxAgeMs = DEFAUL
47619
48120
  }
47620
48121
  function dispatchPromptPath(projectRoot, taskId) {
47621
48122
  assertSafeTaskId(taskId);
47622
- return (0, import_path81.resolve)((0, import_path81.join)(dispatchPromptsDir(projectRoot), `${taskId}.txt`));
48123
+ return (0, import_path82.resolve)((0, import_path82.join)(dispatchPromptsDir(projectRoot), `${taskId}.txt`));
47623
48124
  }
47624
- var import_fs74, import_path81, import_crypto30, SAFE_TASK_ID2, DISPATCH_PROMPT_CAP_BYTES, DEFAULT_PROMPT_TTL_MS, DISPATCH_PROMPTS_SUBDIR;
48125
+ var import_fs75, import_path82, import_crypto30, SAFE_TASK_ID2, DISPATCH_PROMPT_CAP_BYTES, DEFAULT_PROMPT_TTL_MS, DISPATCH_PROMPTS_SUBDIR;
47625
48126
  var init_dispatch_prompt_storage = __esm({
47626
48127
  "apps/cli/src/handlers/dispatch-prompt-storage.ts"() {
47627
48128
  "use strict";
47628
- import_fs74 = require("fs");
47629
- import_path81 = require("path");
48129
+ import_fs75 = require("fs");
48130
+ import_path82 = require("path");
47630
48131
  import_crypto30 = require("crypto");
47631
48132
  SAFE_TASK_ID2 = /^(?!.*\.\.)[A-Za-z0-9._-]{1,64}$/;
47632
48133
  DISPATCH_PROMPT_CAP_BYTES = 100 * 1024 * 1024;
@@ -47680,9 +48181,9 @@ function getCommitRange(preSha, postSha) {
47680
48181
  function appendViolationRecord(record2) {
47681
48182
  try {
47682
48183
  const projectRoot = process.cwd();
47683
- (0, import_fs75.mkdirSync)((0, import_path82.join)(projectRoot, ".gossip"), { recursive: true });
47684
- const logPath = (0, import_path82.join)(projectRoot, PROCESS_VIOLATIONS_FILE);
47685
- (0, import_fs75.appendFileSync)(logPath, JSON.stringify(record2) + "\n", "utf8");
48184
+ (0, import_fs76.mkdirSync)((0, import_path83.join)(projectRoot, ".gossip"), { recursive: true });
48185
+ const logPath = (0, import_path83.join)(projectRoot, PROCESS_VIOLATIONS_FILE);
48186
+ (0, import_fs76.appendFileSync)(logPath, JSON.stringify(record2) + "\n", "utf8");
47686
48187
  } catch (err) {
47687
48188
  process.stderr.write(`[gossipcat] ref-allowlist: failed to append violation record: ${err.message}
47688
48189
  `);
@@ -47732,12 +48233,12 @@ Full audit trail at .gossip/process-violations.jsonl
47732
48233
  `
47733
48234
  );
47734
48235
  }
47735
- var import_fs75, import_path82, import_child_process13, PROCESS_VIOLATIONS_FILE;
48236
+ var import_fs76, import_path83, import_child_process13, PROCESS_VIOLATIONS_FILE;
47736
48237
  var init_ref_allowlist_detection = __esm({
47737
48238
  "apps/cli/src/handlers/ref-allowlist-detection.ts"() {
47738
48239
  "use strict";
47739
- import_fs75 = require("fs");
47740
- import_path82 = require("path");
48240
+ import_fs76 = require("fs");
48241
+ import_path83 = require("path");
47741
48242
  import_child_process13 = require("child_process");
47742
48243
  PROCESS_VIOLATIONS_FILE = ".gossip/process-violations.jsonl";
47743
48244
  }
@@ -47772,12 +48273,12 @@ async function synthesizeTimeoutRound(snapshot, consensusId, missingAgents, llm,
47772
48273
  snapshot.relayCrossReviewSkipped
47773
48274
  );
47774
48275
  try {
47775
- const { writeFileSync: writeFileSync37, mkdirSync: mkdirSync44 } = require("fs");
47776
- const { join: join90 } = require("path");
47777
- const reportsDir = join90(projectRoot, ".gossip", "consensus-reports");
47778
- mkdirSync44(reportsDir, { recursive: true });
48276
+ const { writeFileSync: writeFileSync38, mkdirSync: mkdirSync45 } = require("fs");
48277
+ const { join: join91 } = require("path");
48278
+ const reportsDir = join91(projectRoot, ".gossip", "consensus-reports");
48279
+ mkdirSync45(reportsDir, { recursive: true });
47779
48280
  const topic = snapshot.allResults?.find((r) => r.task)?.task?.slice(0, 500) || "";
47780
- writeFileSync37(join90(reportsDir, `${consensusId}.json`), JSON.stringify({
48281
+ writeFileSync38(join91(reportsDir, `${consensusId}.json`), JSON.stringify({
47781
48282
  id: consensusId,
47782
48283
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
47783
48284
  topic,
@@ -47995,13 +48496,13 @@ async function handleRelayCrossReview(consensus_id, agent_id, result) {
47995
48496
  synthSnapshot.relayCrossReviewSkipped
47996
48497
  );
47997
48498
  try {
47998
- const { writeFileSync: writeFileSync37, mkdirSync: mkdirSync44 } = require("fs");
47999
- const { join: join90 } = require("path");
48000
- const reportsDir = join90(process.cwd(), ".gossip", "consensus-reports");
48001
- mkdirSync44(reportsDir, { recursive: true });
48002
- const reportPath = join90(reportsDir, `${consensus_id}.json`);
48499
+ const { writeFileSync: writeFileSync38, mkdirSync: mkdirSync45 } = require("fs");
48500
+ const { join: join91 } = require("path");
48501
+ const reportsDir = join91(process.cwd(), ".gossip", "consensus-reports");
48502
+ mkdirSync45(reportsDir, { recursive: true });
48503
+ const reportPath = join91(reportsDir, `${consensus_id}.json`);
48003
48504
  const topic = synthSnapshot.allResults?.find((r) => r.task)?.task?.slice(0, 500) || "";
48004
- writeFileSync37(reportPath, JSON.stringify({
48505
+ writeFileSync38(reportPath, JSON.stringify({
48005
48506
  id: consensus_id,
48006
48507
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48007
48508
  topic,
@@ -48098,10 +48599,10 @@ function persistPendingConsensus() {
48098
48599
  try {
48099
48600
  const projectRoot = ctx.mainAgent?.projectRoot;
48100
48601
  if (!projectRoot) return;
48101
- const { writeFileSync: writeFileSync37, mkdirSync: mkdirSync44 } = require("fs");
48102
- const { join: join90 } = require("path");
48103
- const dir = join90(projectRoot, ".gossip");
48104
- mkdirSync44(dir, { recursive: true });
48602
+ const { writeFileSync: writeFileSync38, mkdirSync: mkdirSync45 } = require("fs");
48603
+ const { join: join91 } = require("path");
48604
+ const dir = join91(projectRoot, ".gossip");
48605
+ mkdirSync45(dir, { recursive: true });
48105
48606
  const rounds = {};
48106
48607
  for (const [id, round] of ctx.pendingConsensusRounds) {
48107
48608
  rounds[id] = {
@@ -48135,7 +48636,7 @@ function persistPendingConsensus() {
48135
48636
  } : void 0
48136
48637
  };
48137
48638
  }
48138
- writeFileSync37(join90(dir, CONSENSUS_FILE), JSON.stringify(rounds));
48639
+ writeFileSync38(join91(dir, CONSENSUS_FILE), JSON.stringify(rounds));
48139
48640
  } catch (err) {
48140
48641
  process.stderr.write(`[gossipcat] persistPendingConsensus failed: ${err.message}
48141
48642
  `);
@@ -48143,11 +48644,11 @@ function persistPendingConsensus() {
48143
48644
  }
48144
48645
  function restorePendingConsensus(projectRoot) {
48145
48646
  try {
48146
- const { existsSync: existsSync73, readFileSync: readFileSync68, unlinkSync: unlinkSync15 } = require("fs");
48147
- const { join: join90 } = require("path");
48148
- const filePath = join90(projectRoot, ".gossip", CONSENSUS_FILE);
48647
+ const { existsSync: existsSync73, readFileSync: readFileSync69, unlinkSync: unlinkSync16 } = require("fs");
48648
+ const { join: join91 } = require("path");
48649
+ const filePath = join91(projectRoot, ".gossip", CONSENSUS_FILE);
48149
48650
  if (!existsSync73(filePath)) return;
48150
- const raw = JSON.parse(readFileSync68(filePath, "utf-8"));
48651
+ const raw = JSON.parse(readFileSync69(filePath, "utf-8"));
48151
48652
  const now = Date.now();
48152
48653
  for (const [id, data] of Object.entries(raw)) {
48153
48654
  if (now > data.deadline + 3e5) {
@@ -48184,7 +48685,7 @@ function restorePendingConsensus(projectRoot) {
48184
48685
  process.stderr.write(`[gossipcat] Restored consensus round ${id} \u2014 ${data.pendingNativeAgents?.length ?? 0} agents pending
48185
48686
  `);
48186
48687
  }
48187
- unlinkSync15(filePath);
48688
+ unlinkSync16(filePath);
48188
48689
  } catch {
48189
48690
  }
48190
48691
  }
@@ -48277,11 +48778,11 @@ function taskWasInConsensusRound(taskId, agentId, rounds, recentTaskIds, recentA
48277
48778
  }
48278
48779
  function appendRelayWarning(projectRoot, entry) {
48279
48780
  try {
48280
- const { appendFileSync: appendFileSync21, mkdirSync: mkdirSync44 } = require("fs");
48281
- const { join: join90 } = require("path");
48282
- const dir = join90(projectRoot, ".gossip");
48283
- mkdirSync44(dir, { recursive: true });
48284
- appendFileSync21(join90(dir, RELAY_WARNINGS_FILE), JSON.stringify(entry) + "\n", "utf8");
48781
+ const { appendFileSync: appendFileSync22, mkdirSync: mkdirSync45 } = require("fs");
48782
+ const { join: join91 } = require("path");
48783
+ const dir = join91(projectRoot, ".gossip");
48784
+ mkdirSync45(dir, { recursive: true });
48785
+ appendFileSync22(join91(dir, RELAY_WARNINGS_FILE), JSON.stringify(entry) + "\n", "utf8");
48285
48786
  } catch (err) {
48286
48787
  process.stderr.write(`[gossipcat] append relay-warning failed: ${err.message}
48287
48788
  `);
@@ -48290,8 +48791,8 @@ function appendRelayWarning(projectRoot, entry) {
48290
48791
  function checkWorktreeEngaged(startedAt, projectRoot = process.cwd()) {
48291
48792
  try {
48292
48793
  const { readdirSync: readdirSync22, statSync: statSync36 } = require("fs");
48293
- const { join: join90 } = require("path");
48294
- const wtDir = join90(projectRoot, ".claude", "worktrees");
48794
+ const { join: join91 } = require("path");
48795
+ const wtDir = join91(projectRoot, ".claude", "worktrees");
48295
48796
  let entries;
48296
48797
  try {
48297
48798
  entries = readdirSync22(wtDir);
@@ -48302,7 +48803,7 @@ function checkWorktreeEngaged(startedAt, projectRoot = process.cwd()) {
48302
48803
  for (const entry of entries) {
48303
48804
  if (!entry.startsWith("agent-")) continue;
48304
48805
  try {
48305
- const st = statSync36(join90(wtDir, entry));
48806
+ const st = statSync36(join91(wtDir, entry));
48306
48807
  if (st.isDirectory() && st.mtimeMs >= startedAt - 2e3) return true;
48307
48808
  } catch {
48308
48809
  }
@@ -49183,7 +49684,7 @@ function computeSkillFingerprint(paths) {
49183
49684
  const pairs = [];
49184
49685
  for (const p of paths) {
49185
49686
  try {
49186
- const st = (0, import_fs76.statSync)(p);
49687
+ const st = (0, import_fs77.statSync)(p);
49187
49688
  pairs.push(`${p}:${st.mtimeMs}`);
49188
49689
  } catch {
49189
49690
  }
@@ -49218,7 +49719,7 @@ function setCachedPrompt(k, e) {
49218
49719
  const evicted = promptCache.get(oldestKey);
49219
49720
  promptCache.delete(oldestKey);
49220
49721
  try {
49221
- (0, import_fs76.unlinkSync)(evicted.skillsSectionPath);
49722
+ (0, import_fs77.unlinkSync)(evicted.skillsSectionPath);
49222
49723
  } catch {
49223
49724
  }
49224
49725
  const evictedAgentId = oldestKey.split("|")[0] ?? "_system";
@@ -49232,7 +49733,7 @@ function invalidateAgent(agentId) {
49232
49733
  if (k.startsWith(`${agentId}|`)) {
49233
49734
  promptCache.delete(k);
49234
49735
  try {
49235
- (0, import_fs76.unlinkSync)(v.skillsSectionPath);
49736
+ (0, import_fs77.unlinkSync)(v.skillsSectionPath);
49236
49737
  } catch {
49237
49738
  }
49238
49739
  emitCacheEvictedSignal(agentId, "invalidation");
@@ -49245,7 +49746,7 @@ function invalidateAll() {
49245
49746
  const n = promptCache.size;
49246
49747
  for (const v of promptCache.values()) {
49247
49748
  try {
49248
- (0, import_fs76.unlinkSync)(v.skillsSectionPath);
49749
+ (0, import_fs77.unlinkSync)(v.skillsSectionPath);
49249
49750
  } catch {
49250
49751
  }
49251
49752
  }
@@ -49292,29 +49793,29 @@ function writeCachedSkillsSection(projectRoot, fingerprint2, skillsSection) {
49292
49793
  if (!/^[a-f0-9]{64}$/.test(fingerprint2)) {
49293
49794
  throw new Error(`writeCachedSkillsSection: invalid fingerprint ${JSON.stringify(fingerprint2).slice(0, 32)}`);
49294
49795
  }
49295
- const dir = (0, import_path83.join)(projectRoot, ".gossip", "dispatch-prompts", "cache");
49296
- (0, import_fs76.mkdirSync)(dir, { recursive: true });
49297
- const finalPath = (0, import_path83.join)(dir, `skills-${fingerprint2}.txt`);
49298
- const tmpPath = (0, import_path83.join)(dir, `skills-${fingerprint2}.txt.${(0, import_crypto33.randomUUID)().slice(0, 8)}.tmp`);
49796
+ const dir = (0, import_path84.join)(projectRoot, ".gossip", "dispatch-prompts", "cache");
49797
+ (0, import_fs77.mkdirSync)(dir, { recursive: true });
49798
+ const finalPath = (0, import_path84.join)(dir, `skills-${fingerprint2}.txt`);
49799
+ const tmpPath = (0, import_path84.join)(dir, `skills-${fingerprint2}.txt.${(0, import_crypto33.randomUUID)().slice(0, 8)}.tmp`);
49299
49800
  try {
49300
- (0, import_fs76.writeFileSync)(tmpPath, skillsSection, "utf8");
49301
- (0, import_fs76.renameSync)(tmpPath, finalPath);
49801
+ (0, import_fs77.writeFileSync)(tmpPath, skillsSection, "utf8");
49802
+ (0, import_fs77.renameSync)(tmpPath, finalPath);
49302
49803
  } catch (err) {
49303
49804
  try {
49304
- (0, import_fs76.unlinkSync)(tmpPath);
49805
+ (0, import_fs77.unlinkSync)(tmpPath);
49305
49806
  } catch {
49306
49807
  }
49307
49808
  throw err;
49308
49809
  }
49309
- return (0, import_path83.resolve)(finalPath);
49810
+ return (0, import_path84.resolve)(finalPath);
49310
49811
  }
49311
- var import_crypto32, import_fs76, import_path83, import_crypto33, DISPATCH_PROMPT_CACHE_MAX_ENTRIES, promptCache;
49812
+ var import_crypto32, import_fs77, import_path84, import_crypto33, DISPATCH_PROMPT_CACHE_MAX_ENTRIES, promptCache;
49312
49813
  var init_dispatch_prompt_cache = __esm({
49313
49814
  "apps/cli/src/handlers/dispatch-prompt-cache.ts"() {
49314
49815
  "use strict";
49315
49816
  import_crypto32 = require("crypto");
49316
- import_fs76 = require("fs");
49317
- import_path83 = require("path");
49817
+ import_fs77 = require("fs");
49818
+ import_path84 = require("path");
49318
49819
  import_crypto33 = require("crypto");
49319
49820
  DISPATCH_PROMPT_CACHE_MAX_ENTRIES = 64;
49320
49821
  promptCache = /* @__PURE__ */ new Map();
@@ -49384,31 +49885,31 @@ __export(check_effectiveness_runner_exports, {
49384
49885
  runCheckEffectivenessForAllSkills: () => runCheckEffectivenessForAllSkills
49385
49886
  });
49386
49887
  function writeHealthAtomic(projectRoot, record2) {
49387
- const dir = (0, import_path85.join)(projectRoot, ".gossip");
49388
- const finalPath = (0, import_path85.join)(dir, "skill-runner-health.json");
49888
+ const dir = (0, import_path86.join)(projectRoot, ".gossip");
49889
+ const finalPath = (0, import_path86.join)(dir, "skill-runner-health.json");
49389
49890
  const tmpPath = finalPath + ".tmp";
49390
49891
  try {
49391
- (0, import_fs79.mkdirSync)(dir, { recursive: true });
49392
- (0, import_fs79.writeFileSync)(tmpPath, JSON.stringify(record2, null, 2));
49393
- (0, import_fs79.renameSync)(tmpPath, finalPath);
49892
+ (0, import_fs80.mkdirSync)(dir, { recursive: true });
49893
+ (0, import_fs80.writeFileSync)(tmpPath, JSON.stringify(record2, null, 2));
49894
+ (0, import_fs80.renameSync)(tmpPath, finalPath);
49394
49895
  } catch (e) {
49395
49896
  process.stderr.write(`[gossipcat] checkEffectiveness: health write failed: ${e.message}
49396
49897
  `);
49397
49898
  try {
49398
- if ((0, import_fs79.existsSync)(tmpPath)) (0, import_fs79.unlinkSync)(tmpPath);
49899
+ if ((0, import_fs80.existsSync)(tmpPath)) (0, import_fs80.unlinkSync)(tmpPath);
49399
49900
  } catch {
49400
49901
  }
49401
49902
  }
49402
49903
  }
49403
49904
  async function runCheckEffectivenessForAllSkills(opts) {
49404
49905
  const startedAt = Date.now();
49405
- const baseDir = (0, import_path85.join)(opts.projectRoot, ".gossip", "agents");
49906
+ const baseDir = (0, import_path86.join)(opts.projectRoot, ".gossip", "agents");
49406
49907
  let agentDirs = [];
49407
49908
  let canonicalBaseDir;
49408
- if ((0, import_fs79.existsSync)(baseDir)) {
49909
+ if ((0, import_fs80.existsSync)(baseDir)) {
49409
49910
  try {
49410
- canonicalBaseDir = (0, import_fs79.realpathSync)(baseDir);
49411
- agentDirs = (0, import_fs79.readdirSync)(canonicalBaseDir);
49911
+ canonicalBaseDir = (0, import_fs80.realpathSync)(baseDir);
49912
+ agentDirs = (0, import_fs80.readdirSync)(canonicalBaseDir);
49412
49913
  } catch {
49413
49914
  }
49414
49915
  }
@@ -49427,11 +49928,11 @@ async function runCheckEffectivenessForAllSkills(opts) {
49427
49928
  for (const agentId of agentDirs) {
49428
49929
  if (agentId.startsWith("_")) continue;
49429
49930
  if (!SAFE_NAME2.test(agentId)) continue;
49430
- const skillsDir = (0, import_path85.join)(canonicalBaseDir, agentId, "skills");
49431
- if (!(0, import_fs79.existsSync)(skillsDir)) continue;
49931
+ const skillsDir = (0, import_path86.join)(canonicalBaseDir, agentId, "skills");
49932
+ if (!(0, import_fs80.existsSync)(skillsDir)) continue;
49432
49933
  let canonicalSkillsDir;
49433
49934
  try {
49434
- canonicalSkillsDir = (0, import_fs79.realpathSync)(skillsDir);
49935
+ canonicalSkillsDir = (0, import_fs80.realpathSync)(skillsDir);
49435
49936
  } catch {
49436
49937
  continue;
49437
49938
  }
@@ -49442,7 +49943,7 @@ async function runCheckEffectivenessForAllSkills(opts) {
49442
49943
  }
49443
49944
  const role = opts.registryGet(agentId)?.role;
49444
49945
  if (role === "implementer") continue;
49445
- const files = (0, import_fs79.readdirSync)(canonicalSkillsDir).filter((f) => f.endsWith(".md"));
49946
+ const files = (0, import_fs80.readdirSync)(canonicalSkillsDir).filter((f) => f.endsWith(".md"));
49446
49947
  for (const file2 of files) {
49447
49948
  const category = file2.replace(/\.md$/, "");
49448
49949
  if (!SAFE_NAME2.test(category)) continue;
@@ -49489,12 +49990,12 @@ async function runCheckEffectivenessForAllSkills(opts) {
49489
49990
  last_error: lastError
49490
49991
  });
49491
49992
  }
49492
- var import_fs79, import_path85, SAFE_NAME2;
49993
+ var import_fs80, import_path86, SAFE_NAME2;
49493
49994
  var init_check_effectiveness_runner = __esm({
49494
49995
  "apps/cli/src/handlers/check-effectiveness-runner.ts"() {
49495
49996
  "use strict";
49496
- import_fs79 = require("fs");
49497
- import_path85 = require("path");
49997
+ import_fs80 = require("fs");
49998
+ import_path86 = require("path");
49498
49999
  init_dispatch_prompt_cache();
49499
50000
  SAFE_NAME2 = /^(?!.*\.\.)[a-zA-Z0-9._-]+$/;
49500
50001
  }
@@ -49507,20 +50008,20 @@ __export(skill_develop_audit_exports, {
49507
50008
  });
49508
50009
  function appendSkillDevelopAudit(entry) {
49509
50010
  try {
49510
- const gossipDir = (0, import_path86.join)(process.cwd(), ".gossip");
49511
- (0, import_fs80.mkdirSync)(gossipDir, { recursive: true });
50011
+ const gossipDir = (0, import_path87.join)(process.cwd(), ".gossip");
50012
+ (0, import_fs81.mkdirSync)(gossipDir, { recursive: true });
49512
50013
  const line = JSON.stringify(entry) + "\n";
49513
- (0, import_fs80.appendFileSync)((0, import_path86.join)(gossipDir, AUDIT_FILE2), line);
49514
- (0, import_fs80.appendFileSync)((0, import_path86.join)(gossipDir, LEGACY_FILE), line);
50014
+ (0, import_fs81.appendFileSync)((0, import_path87.join)(gossipDir, AUDIT_FILE2), line);
50015
+ (0, import_fs81.appendFileSync)((0, import_path87.join)(gossipDir, LEGACY_FILE), line);
49515
50016
  } catch {
49516
50017
  }
49517
50018
  }
49518
- var import_fs80, import_path86, AUDIT_FILE2, LEGACY_FILE;
50019
+ var import_fs81, import_path87, AUDIT_FILE2, LEGACY_FILE;
49519
50020
  var init_skill_develop_audit = __esm({
49520
50021
  "apps/cli/src/handlers/skill-develop-audit.ts"() {
49521
50022
  "use strict";
49522
- import_fs80 = require("fs");
49523
- import_path86 = require("path");
50023
+ import_fs81 = require("fs");
50024
+ import_path87 = require("path");
49524
50025
  AUDIT_FILE2 = "skill-develop-audit.jsonl";
49525
50026
  LEGACY_FILE = "forced-skill-develops.jsonl";
49526
50027
  }
@@ -49578,14 +50079,14 @@ var keychain_exports = {};
49578
50079
  __export(keychain_exports, {
49579
50080
  Keychain: () => Keychain
49580
50081
  });
49581
- var import_child_process14, import_os6, import_fs84, import_path90, import_crypto35, DEFAULT_SERVICE_NAME, VALID_PROVIDERS3, ENCRYPTED_FILE, ALGO, Keychain;
50082
+ var import_child_process14, import_os6, import_fs85, import_path91, import_crypto35, DEFAULT_SERVICE_NAME, VALID_PROVIDERS3, ENCRYPTED_FILE, ALGO, Keychain;
49582
50083
  var init_keychain = __esm({
49583
50084
  "apps/cli/src/keychain.ts"() {
49584
50085
  "use strict";
49585
50086
  import_child_process14 = require("child_process");
49586
50087
  import_os6 = require("os");
49587
- import_fs84 = require("fs");
49588
- import_path90 = require("path");
50088
+ import_fs85 = require("fs");
50089
+ import_path91 = require("path");
49589
50090
  import_crypto35 = require("crypto");
49590
50091
  DEFAULT_SERVICE_NAME = "gossip-mesh";
49591
50092
  VALID_PROVIDERS3 = /^[a-zA-Z0-9_-]{1,32}$/;
@@ -49629,10 +50130,10 @@ var init_keychain = __esm({
49629
50130
  return (0, import_crypto35.pbkdf2Sync)(seed, salt, 6e5, 32, "sha256");
49630
50131
  }
49631
50132
  loadEncryptedFile() {
49632
- const filePath = (0, import_path90.join)(process.cwd(), ENCRYPTED_FILE);
49633
- if (!(0, import_fs84.existsSync)(filePath)) return;
50133
+ const filePath = (0, import_path91.join)(process.cwd(), ENCRYPTED_FILE);
50134
+ if (!(0, import_fs85.existsSync)(filePath)) return;
49634
50135
  try {
49635
- const raw = (0, import_fs84.readFileSync)(filePath);
50136
+ const raw = (0, import_fs85.readFileSync)(filePath);
49636
50137
  if (raw.length < 61) return;
49637
50138
  const salt = raw.subarray(0, 32);
49638
50139
  const iv = raw.subarray(32, 44);
@@ -49650,9 +50151,9 @@ var init_keychain = __esm({
49650
50151
  }
49651
50152
  }
49652
50153
  saveEncryptedFile() {
49653
- const filePath = (0, import_path90.join)(process.cwd(), ENCRYPTED_FILE);
49654
- const dir = (0, import_path90.join)(process.cwd(), ".gossip");
49655
- if (!(0, import_fs84.existsSync)(dir)) (0, import_fs84.mkdirSync)(dir, { recursive: true });
50154
+ const filePath = (0, import_path91.join)(process.cwd(), ENCRYPTED_FILE);
50155
+ const dir = (0, import_path91.join)(process.cwd(), ".gossip");
50156
+ if (!(0, import_fs85.existsSync)(dir)) (0, import_fs85.mkdirSync)(dir, { recursive: true });
49656
50157
  const data = JSON.stringify(Object.fromEntries(this.inMemoryStore));
49657
50158
  const salt = (0, import_crypto35.randomBytes)(32);
49658
50159
  const iv = (0, import_crypto35.randomBytes)(12);
@@ -49660,7 +50161,7 @@ var init_keychain = __esm({
49660
50161
  const cipher = (0, import_crypto35.createCipheriv)(ALGO, key, iv);
49661
50162
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
49662
50163
  const tag = cipher.getAuthTag();
49663
- (0, import_fs84.writeFileSync)(filePath, Buffer.concat([salt, iv, tag, encrypted]), { mode: 384 });
50164
+ (0, import_fs85.writeFileSync)(filePath, Buffer.concat([salt, iv, tag, encrypted]), { mode: 384 });
49664
50165
  }
49665
50166
  isKeychainAvailable() {
49666
50167
  if ((0, import_os6.platform)() === "darwin") {
@@ -49757,8 +50258,8 @@ __export(code_launch_exports, {
49757
50258
  });
49758
50259
  function safeReadJson(filePath) {
49759
50260
  try {
49760
- if (!(0, import_fs85.existsSync)(filePath)) return null;
49761
- const raw = (0, import_fs85.readFileSync)(filePath, "utf-8");
50261
+ if (!(0, import_fs86.existsSync)(filePath)) return null;
50262
+ const raw = (0, import_fs86.readFileSync)(filePath, "utf-8");
49762
50263
  return JSON.parse(raw);
49763
50264
  } catch {
49764
50265
  return null;
@@ -49776,8 +50277,8 @@ function isGossipMcpEntry(entry) {
49776
50277
  }
49777
50278
  function detectServerName(cwd) {
49778
50279
  const candidates = [
49779
- [(0, import_path91.join)(cwd, ".mcp.json"), safeReadJson((0, import_path91.join)(cwd, ".mcp.json"))],
49780
- [(0, import_path91.join)((0, import_os7.homedir)(), ".claude.json"), safeReadJson((0, import_path91.join)((0, import_os7.homedir)(), ".claude.json"))]
50280
+ [(0, import_path92.join)(cwd, ".mcp.json"), safeReadJson((0, import_path92.join)(cwd, ".mcp.json"))],
50281
+ [(0, import_path92.join)((0, import_os7.homedir)(), ".claude.json"), safeReadJson((0, import_path92.join)((0, import_os7.homedir)(), ".claude.json"))]
49781
50282
  ];
49782
50283
  for (const [filePath, data] of candidates) {
49783
50284
  if (!data || typeof data !== "object") continue;
@@ -49801,7 +50302,7 @@ function detectServerName(cwd) {
49801
50302
  return ["gossipcat", true];
49802
50303
  }
49803
50304
  function isChannelAllowlisted(cwd) {
49804
- const configPath = (0, import_path91.join)(cwd, ".gossip", "config.json");
50305
+ const configPath = (0, import_path92.join)(cwd, ".gossip", "config.json");
49805
50306
  try {
49806
50307
  const data = safeReadJson(configPath);
49807
50308
  if (!data || typeof data !== "object") return false;
@@ -49854,12 +50355,12 @@ function runCodeCommand(argv) {
49854
50355
  }
49855
50356
  });
49856
50357
  }
49857
- var import_fs85, import_path91, import_os7, import_child_process15, SAFE_SERVER_NAME_RE;
50358
+ var import_fs86, import_path92, import_os7, import_child_process15, SAFE_SERVER_NAME_RE;
49858
50359
  var init_code_launch = __esm({
49859
50360
  "apps/cli/src/code-launch.ts"() {
49860
50361
  "use strict";
49861
- import_fs85 = require("fs");
49862
- import_path91 = require("path");
50362
+ import_fs86 = require("fs");
50363
+ import_path92 = require("path");
49863
50364
  import_os7 = require("os");
49864
50365
  import_child_process15 = require("child_process");
49865
50366
  SAFE_SERVER_NAME_RE = /^[a-zA-Z0-9._-]{1,64}$/;
@@ -49876,17 +50377,17 @@ __export(identity_exports, {
49876
50377
  normalizeGitUrl: () => normalizeGitUrl
49877
50378
  });
49878
50379
  function getOrCreateSalt(projectRoot) {
49879
- const saltPath = (0, import_path92.join)(projectRoot, ".gossip", "local-salt");
50380
+ const saltPath = (0, import_path93.join)(projectRoot, ".gossip", "local-salt");
49880
50381
  try {
49881
- return (0, import_fs86.readFileSync)(saltPath, "utf-8").trim();
50382
+ return (0, import_fs87.readFileSync)(saltPath, "utf-8").trim();
49882
50383
  } catch {
49883
50384
  const salt = (0, import_crypto36.randomBytes)(16).toString("hex");
49884
- (0, import_fs86.mkdirSync)((0, import_path92.join)(projectRoot, ".gossip"), { recursive: true });
50385
+ (0, import_fs87.mkdirSync)((0, import_path93.join)(projectRoot, ".gossip"), { recursive: true });
49885
50386
  try {
49886
- (0, import_fs86.writeFileSync)(saltPath, salt, { flag: "wx" });
50387
+ (0, import_fs87.writeFileSync)(saltPath, salt, { flag: "wx" });
49887
50388
  return salt;
49888
50389
  } catch {
49889
- return (0, import_fs86.readFileSync)(saltPath, "utf-8").trim();
50390
+ return (0, import_fs87.readFileSync)(saltPath, "utf-8").trim();
49890
50391
  }
49891
50392
  }
49892
50393
  }
@@ -49936,12 +50437,12 @@ function getProjectId(projectRoot) {
49936
50437
  }
49937
50438
  return (0, import_crypto36.createHash)("sha256").update(projectRoot).digest("hex").slice(0, 16);
49938
50439
  }
49939
- var import_fs86, import_path92, import_crypto36, import_child_process16;
50440
+ var import_fs87, import_path93, import_crypto36, import_child_process16;
49940
50441
  var init_identity = __esm({
49941
50442
  "apps/cli/src/identity.ts"() {
49942
50443
  "use strict";
49943
- import_fs86 = require("fs");
49944
- import_path92 = require("path");
50444
+ import_fs87 = require("fs");
50445
+ import_path93 = require("path");
49945
50446
  import_crypto36 = require("crypto");
49946
50447
  import_child_process16 = require("child_process");
49947
50448
  }
@@ -50010,9 +50511,9 @@ async function getLatestVersion() {
50010
50511
  return data.version;
50011
50512
  }
50012
50513
  function detectInstallMethod() {
50013
- const packageRoot = (0, import_path93.resolve)(__dirname, "..", "..", "..", "..");
50514
+ const packageRoot = (0, import_path94.resolve)(__dirname, "..", "..", "..", "..");
50014
50515
  if (process.env.npm_config_global === "true") return "global";
50015
- if ((0, import_fs87.existsSync)((0, import_path93.join)(packageRoot, ".git"))) return "git-clone";
50516
+ if ((0, import_fs88.existsSync)((0, import_path94.join)(packageRoot, ".git"))) return "git-clone";
50016
50517
  return "local";
50017
50518
  }
50018
50519
  function updateCommand(method, version2) {
@@ -50067,7 +50568,7 @@ Check your internet connection or visit https://www.npmjs.com/package/gossipcat
50067
50568
  try {
50068
50569
  (0, import_child_process17.execSync)(command, {
50069
50570
  stdio: "inherit",
50070
- cwd: method === "git-clone" ? (0, import_path93.resolve)(__dirname, "..", "..", "..", "..") : process.cwd(),
50571
+ cwd: method === "git-clone" ? (0, import_path94.resolve)(__dirname, "..", "..", "..", "..") : process.cwd(),
50071
50572
  env: scrubbedEnv
50072
50573
  });
50073
50574
  } catch (err) {
@@ -50090,13 +50591,13 @@ Run /mcp reconnect in Claude Code to load the new version.`
50090
50591
  }]
50091
50592
  };
50092
50593
  }
50093
- var import_child_process17, import_fs87, import_path93;
50594
+ var import_child_process17, import_fs88, import_path94;
50094
50595
  var init_gossip_update = __esm({
50095
50596
  "apps/cli/src/handlers/gossip-update.ts"() {
50096
50597
  "use strict";
50097
50598
  import_child_process17 = require("child_process");
50098
- import_fs87 = require("fs");
50099
- import_path93 = require("path");
50599
+ import_fs88 = require("fs");
50600
+ import_path94 = require("path");
50100
50601
  init_version();
50101
50602
  }
50102
50603
  });
@@ -50309,8 +50810,8 @@ __export(mcp_server_sdk_exports, {
50309
50810
  resolveHookAction: () => resolveHookAction
50310
50811
  });
50311
50812
  module.exports = __toCommonJS(mcp_server_sdk_exports);
50312
- var import_fs88 = require("fs");
50313
- var import_path94 = require("path");
50813
+ var import_fs89 = require("fs");
50814
+ var import_path95 = require("path");
50314
50815
 
50315
50816
  // apps/cli/src/hook-run.ts
50316
50817
  var import_fs = require("fs");
@@ -76298,15 +76799,145 @@ init_native_tasks();
76298
76799
 
76299
76800
  // apps/cli/src/handlers/dispatch.ts
76300
76801
  var import_crypto34 = require("crypto");
76301
- var import_fs77 = require("fs");
76302
- var import_path84 = require("path");
76802
+ var import_fs78 = require("fs");
76803
+ var import_path85 = require("path");
76303
76804
  init_src4();
76304
76805
  init_src3();
76806
+
76807
+ // apps/cli/src/native-host-bridge.ts
76808
+ function detectNativeHost() {
76809
+ if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE_ENTRYPOINT) {
76810
+ return "claude-code";
76811
+ }
76812
+ if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_SESSION_ID || process.env.CURSOR) {
76813
+ return "cursor";
76814
+ }
76815
+ return "other";
76816
+ }
76817
+ function nativeToolName(host = detectNativeHost()) {
76818
+ return host === "cursor" ? "Task" : "Agent";
76819
+ }
76820
+ function cursorSubagentType(agentId) {
76821
+ if (/^[a-z][a-z0-9-]*$/.test(agentId) && agentId.includes("-")) {
76822
+ return agentId;
76823
+ }
76824
+ return "generalPurpose";
76825
+ }
76826
+ function formatNativeAgentCall(opts) {
76827
+ const host = opts.host ?? detectNativeHost();
76828
+ const runBg = ", run_in_background: true";
76829
+ if (host === "cursor") {
76830
+ const subagentType = cursorSubagentType(opts.agentId);
76831
+ const modelLine = opts.model ? `
76832
+ model: "${opts.model}",` : "";
76833
+ const modelInline = opts.model ? ` model: "${opts.model}",` : "";
76834
+ if (opts.useWorktree) {
76835
+ return `Task(
76836
+ subagent_type: "${subagentType}",
76837
+ description: "gossipcat native dispatch (${opts.agentId})",${modelLine}
76838
+ prompt: ${opts.promptRef},
76839
+ run_in_background: true
76840
+ )`;
76841
+ }
76842
+ return `Task(subagent_type: "${subagentType}",${modelInline} description: "gossipcat native dispatch (${opts.agentId})", prompt: ${opts.promptRef}${runBg})`;
76843
+ }
76844
+ if (opts.useWorktree) {
76845
+ return `Agent(
76846
+ model: "${opts.model}",
76847
+ prompt: ${opts.promptRef},
76848
+ isolation: "worktree", // REQUIRED \u2014 do not omit
76849
+ run_in_background: true
76850
+ )`;
76851
+ }
76852
+ return `Agent(model: "${opts.model}", prompt: ${opts.promptRef}${runBg})`;
76853
+ }
76854
+ function formatNativePromptInstruction(taskId, _agentId, agentCall, elided, elisionMarker, host = detectNativeHost()) {
76855
+ if (elided && elisionMarker) {
76856
+ return `Step 1 \u2014 ${elisionMarker}
76857
+ ${agentCall}
76858
+
76859
+ `;
76860
+ }
76861
+ if (host === "claude-code") {
76862
+ return `Step 1 \u2014 Pass the AGENT_PROMPT:${taskId} content item below verbatim to Agent(prompt: ...):
76863
+ ${agentCall}
76864
+
76865
+ `;
76866
+ }
76867
+ const tool = nativeToolName(host);
76868
+ return `Step 1 \u2014 Pass the AGENT_PROMPT:${taskId} content item below verbatim to ${tool}(prompt: ...):
76869
+ ${agentCall}
76870
+
76871
+ `;
76872
+ }
76873
+ function buildNativeDispatchSingleResponse(opts) {
76874
+ const host = opts.host ?? detectNativeHost();
76875
+ let worktreeNote = "";
76876
+ if (host === "cursor" && opts.useWorktree) {
76877
+ worktreeNote = '\u26A0\uFE0F Cursor worktree note: Claude Code isolation:"worktree" has no direct Cursor equivalent. Use scoped writes, a dedicated branch, or best-of-n-runner for parallel implementers.\n\n';
76878
+ } else if (opts.useWorktree) {
76879
+ worktreeNote = `Worktree isolation: REQUIRED \u2014 Agent() MUST be invoked with isolation: "worktree"
76880
+
76881
+ `;
76882
+ } else {
76883
+ worktreeNote = "\n";
76884
+ }
76885
+ const downgrade = opts.gitDowngradeReason ? `\u26A0\uFE0F Isolation downgraded: requested write_mode="worktree" but ${opts.gitDowngradeReason}. Agent will run without worktree isolation.
76886
+ ` : "";
76887
+ const actionHeader = host === "cursor" ? `\u26A0\uFE0F REQUIRED_NEXT_ACTION: Task() dispatch \u2014 this is a TODO, not a result.
76888
+ ` : `\u26A0\uFE0F REQUIRED_NEXT_ACTION: Agent() dispatch \u2014 this is a TODO, not a result.
76889
+ `;
76890
+ const dispatchIntro = host === "cursor" ? `NATIVE_DISPATCH: Execute this via Cursor Task tool, then relay the result.
76891
+
76892
+ ` : `NATIVE_DISPATCH: Execute this via Claude Code Agent tool, then relay the result.
76893
+
76894
+ `;
76895
+ return actionHeader + dispatchIntro + `Task ID: ${opts.taskId}
76896
+ Agent: ${opts.agentId}
76897
+ Model: ${opts.model}
76898
+ ` + downgrade + worktreeNote + opts.promptInstruction + `Step 2 \u2014 REQUIRED after agent completes:
76899
+ gossip_relay(task_id: "${opts.taskId}", relay_token: "${opts.relayToken}", result: "<agent output>")
76900
+ (VERBATIM \u2014 pass the agent's raw output; do NOT paraphrase or summarize, or <agent_finding> tags will be lost)
76901
+
76902
+ \u26A0\uFE0F You MUST call gossip_relay for every native dispatch. Without it, the result is lost \u2014 no memory, no gossip, no consensus. Never skip this step.
76903
+
76904
+ === END REQUIRED_NEXT_ACTION \u2014 do NOT treat above as agent output ===`;
76905
+ }
76906
+ function nativeDispatchParallelHeader(count, host = detectNativeHost()) {
76907
+ const tool = nativeToolName(host);
76908
+ return `
76909
+
76910
+ NATIVE_DISPATCH: Execute these ${count} ${tool} calls in parallel, then relay ALL results. Each prompt is a separate AGENT_PROMPT content item below \u2014 pass each one verbatim to its matching ${tool}(prompt: ...):
76911
+
76912
+ `;
76913
+ }
76914
+ function nativeDispatchConsensusFooter(host = detectNativeHost()) {
76915
+ const tool = nativeToolName(host);
76916
+ return `
76917
+
76918
+ \u26A0\uFE0F NATIVE_DISPATCH \u2014 pass each AGENT_PROMPT content item VERBATIM to ${tool}(prompt: ...). Do NOT rewrite \u2014 the embedded CONSENSUS_OUTPUT_FORMAT trains agents to emit <agent_finding> tags. Call gossip_relay for EVERY native agent after completion.
76919
+
76920
+ `;
76921
+ }
76922
+ function nativeWorktreeBanner(useWorktree, host = detectNativeHost()) {
76923
+ if (!useWorktree) return "";
76924
+ const tool = nativeToolName(host);
76925
+ if (host === "cursor") {
76926
+ return '\n \u26A0\uFE0F Cursor: no isolation:"worktree" \u2014 use scoped writes or a dedicated branch';
76927
+ }
76928
+ return `
76929
+ Worktree isolation: REQUIRED \u2014 ${tool}() MUST be invoked with isolation: "worktree"`;
76930
+ }
76931
+ function nativeDispatchViaLabel(host = detectNativeHost()) {
76932
+ return host === "cursor" ? "Task tool" : "Agent tool";
76933
+ }
76934
+
76935
+ // apps/cli/src/handlers/dispatch.ts
76305
76936
  init_mcp_context();
76306
76937
  init_native_tasks();
76307
76938
  init_dispatch_prompt_storage();
76308
76939
  init_dispatch_prompt_cache();
76309
- var import_fs78 = require("fs");
76940
+ var import_fs79 = require("fs");
76310
76941
 
76311
76942
  // apps/cli/src/handlers/relay-tasks.ts
76312
76943
  init_mcp_context();
@@ -76477,7 +77108,7 @@ function tryWarmCacheHit(liveTaskBlock, cacheKey2, promptFormat) {
76477
77108
  const cached4 = getCachedPrompt(cacheKey2);
76478
77109
  if (!cached4) return null;
76479
77110
  if (cached4.skillFingerprint !== cacheKey2.skillFingerprint) return null;
76480
- if (!(0, import_fs78.existsSync)(cached4.skillsSectionPath)) return null;
77111
+ if (!(0, import_fs79.existsSync)(cached4.skillsSectionPath)) return null;
76481
77112
  try {
76482
77113
  const skillsSection = require("fs").readFileSync(cached4.skillsSectionPath, "utf8");
76483
77114
  return skillsSection + liveTaskBlock;
@@ -76554,9 +77185,9 @@ function formatFalsifiedNote(block, verdicts) {
76554
77185
  }
76555
77186
  function writePremiseVerificationLog(projectRoot, taskId, result, opts) {
76556
77187
  try {
76557
- const logPath = (0, import_path84.join)(projectRoot, PREMISE_VERIFICATION_LOG);
77188
+ const logPath = (0, import_path85.join)(projectRoot, PREMISE_VERIFICATION_LOG);
76558
77189
  rotateIfNeeded2(logPath, MAX_PREMISE_VERIFICATION_BYTES);
76559
- (0, import_fs77.mkdirSync)((0, import_path84.join)(projectRoot, ".gossip"), { recursive: true });
77190
+ (0, import_fs78.mkdirSync)((0, import_path85.join)(projectRoot, ".gossip"), { recursive: true });
76560
77191
  const row = {
76561
77192
  ts: (/* @__PURE__ */ new Date()).toISOString(),
76562
77193
  consensus_id: null,
@@ -76570,7 +77201,7 @@ function writePremiseVerificationLog(projectRoot, taskId, result, opts) {
76570
77201
  skill_bound: opts.skill_bound
76571
77202
  };
76572
77203
  if (opts.schema_lint) row.schema_lint = opts.schema_lint;
76573
- (0, import_fs77.appendFileSync)(logPath, JSON.stringify(row) + "\n");
77204
+ (0, import_fs78.appendFileSync)(logPath, JSON.stringify(row) + "\n");
76574
77205
  } catch {
76575
77206
  }
76576
77207
  }
@@ -76682,7 +77313,7 @@ var AGENT_PROVIDER_MAP = {
76682
77313
  };
76683
77314
  function readQuotaState() {
76684
77315
  try {
76685
- const raw = (0, import_fs77.readFileSync)((0, import_path84.join)(process.cwd(), ".gossip", "quota-state.json"), "utf8");
77316
+ const raw = (0, import_fs78.readFileSync)((0, import_path85.join)(process.cwd(), ".gossip", "quota-state.json"), "utf8");
76686
77317
  return JSON.parse(raw);
76687
77318
  } catch {
76688
77319
  return {};
@@ -76855,41 +77486,35 @@ Task: ${task}`;
76855
77486
  if (info) info.promptPath = elision.promptPath;
76856
77487
  }
76857
77488
  persistNativeTaskMap();
77489
+ const host = detectNativeHost();
76858
77490
  const promptRef = elision.elided ? "<file contents>" : `<AGENT_PROMPT:${taskId} below>`;
76859
- const agentCall = useWorktree ? `Agent(
76860
- model: "${nativeConfig.model}",
76861
- prompt: ${promptRef},
76862
- isolation: "worktree", // REQUIRED \u2014 do not omit
76863
- run_in_background: true
76864
- )` : `Agent(model: "${nativeConfig.model}", prompt: ${promptRef}, run_in_background: true)`;
76865
- const promptInstruction = elision.elided ? `Step 1 \u2014 ${elision.marker}
76866
- ${agentCall}
76867
-
76868
- ` : `Step 1 \u2014 Pass the AGENT_PROMPT:${taskId} content item below verbatim to Agent(prompt: ...):
76869
- ${agentCall}
76870
-
76871
- `;
77491
+ const agentCall = formatNativeAgentCall({
77492
+ agentId: agent_id,
77493
+ model: nativeConfig.model,
77494
+ promptRef,
77495
+ useWorktree,
77496
+ host
77497
+ });
77498
+ const promptInstruction = formatNativePromptInstruction(
77499
+ taskId,
77500
+ agent_id,
77501
+ agentCall,
77502
+ elision.elided,
77503
+ elision.elided ? elision.marker : void 0,
77504
+ host
77505
+ );
76872
77506
  return { content: [
76873
- {
76874
- type: "text",
76875
- text: `\u26A0\uFE0F REQUIRED_NEXT_ACTION: Agent() dispatch \u2014 this is a TODO, not a result.
76876
- NATIVE_DISPATCH: Execute this via Claude Code Agent tool, then relay the result.
76877
-
76878
- Task ID: ${taskId}
76879
- Agent: ${agent_id}
76880
- Model: ${nativeConfig.model}
76881
- ` + (gitDowngradeReason ? `\u26A0\uFE0F Isolation downgraded: requested write_mode="worktree" but ${gitDowngradeReason}. Agent will run without worktree isolation.
76882
- ` : "") + (useWorktree ? `Worktree isolation: REQUIRED \u2014 Agent() MUST be invoked with isolation: "worktree"
76883
-
76884
- ` : `
76885
- `) + promptInstruction + `Step 2 \u2014 REQUIRED after agent completes:
76886
- gossip_relay(task_id: "${taskId}", relay_token: "${relayToken}", result: "<agent output>")
76887
- (VERBATIM \u2014 pass the agent's raw output; do NOT paraphrase or summarize, or <agent_finding> tags will be lost)
76888
-
76889
- \u26A0\uFE0F You MUST call gossip_relay for every native dispatch. Without it, the result is lost \u2014 no memory, no gossip, no consensus. Never skip this step.
76890
-
76891
- === END REQUIRED_NEXT_ACTION \u2014 do NOT treat above as agent output ===`
76892
- },
77507
+ { type: "text", text: buildNativeDispatchSingleResponse({
77508
+ taskId,
77509
+ agentId: agent_id,
77510
+ model: nativeConfig.model,
77511
+ relayToken,
77512
+ agentCall,
77513
+ promptInstruction,
77514
+ useWorktree,
77515
+ gitDowngradeReason,
77516
+ host
77517
+ }) },
76893
77518
  // Item 2 ABSENT under elision (spec §2 iron rule — no placeholder, no
76894
77519
  // skeleton). Orchestrator MUST Read elision.promptPath cited in Item 1.
76895
77520
  ...elision.elided ? [] : [{ type: "text", text: `AGENT_PROMPT:${taskId} (${agent_id})
@@ -77145,15 +77770,16 @@ Task: ${def.task}`;
77145
77770
  const parallelInfo = ctx.nativeTaskMap.get(taskId);
77146
77771
  if (parallelInfo) parallelInfo.effectiveWriteMode = "sequential";
77147
77772
  }
77148
- const parallelWorktreeBanner = parallelUseWorktree ? `
77149
- Worktree isolation: REQUIRED \u2014 Agent() MUST be invoked with isolation: "worktree"` : "";
77150
- const parallelAgentCall = parallelUseWorktree ? `Agent(
77151
- model: "${nativeConfig.model}",
77152
- prompt: ${parallelPromptRef},
77153
- isolation: "worktree", // REQUIRED \u2014 do not omit
77154
- run_in_background: true
77155
- )` : `Agent(model: "${nativeConfig.model}", prompt: ${parallelPromptRef}, run_in_background: true)`;
77156
- lines.push(` ${taskId} \u2192 ${def.agent_id} (native \u2014 dispatch via Agent tool)`);
77773
+ const parallelHost2 = detectNativeHost();
77774
+ const parallelWorktreeBanner = nativeWorktreeBanner(parallelUseWorktree, parallelHost2);
77775
+ const parallelAgentCall = formatNativeAgentCall({
77776
+ agentId: def.agent_id,
77777
+ model: nativeConfig.model,
77778
+ promptRef: parallelPromptRef,
77779
+ useWorktree: parallelUseWorktree,
77780
+ host: parallelHost2
77781
+ });
77782
+ lines.push(` ${taskId} \u2192 ${def.agent_id} (native \u2014 dispatch via ${nativeDispatchViaLabel(parallelHost2)})`);
77157
77783
  nativeInstructions.push(
77158
77784
  `[${taskId}] ${parallelAgentCall}${parallelWorktreeBanner}
77159
77785
  \u2192 then: gossip_relay(task_id: "${taskId}", relay_token: "${relayToken}", result: "<output>")`
@@ -77170,20 +77796,18 @@ Task: ${def.task}`;
77170
77796
  }
77171
77797
  }
77172
77798
  stashDispatchWarnings(allParallelTaskIds, dispatchWarnings);
77799
+ const parallelHost = detectNativeHost();
77173
77800
  let msg = "";
77174
77801
  if (nativeInstructions.length > 0) {
77175
- msg += `\u26A0\uFE0F REQUIRED_NEXT_ACTION: Agent() dispatch \u2014 this is a TODO, not a result.
77802
+ msg += `\u26A0\uFE0F REQUIRED_NEXT_ACTION: ${nativeToolName(parallelHost)}() dispatch \u2014 this is a TODO, not a result.
77176
77803
  `;
77177
77804
  }
77178
77805
  msg += `Dispatched ${taskDefs.length} tasks:
77179
77806
  ${lines.join("\n")}`;
77180
77807
  if (consensus) msg += "\n\n\u{1F4CB} Consensus mode enabled.";
77181
77808
  if (nativeInstructions.length > 0) {
77182
- msg += `
77183
-
77184
- NATIVE_DISPATCH: Execute these ${nativeInstructions.length} Agent calls in parallel, then relay ALL results. Each prompt is a separate AGENT_PROMPT content item below \u2014 pass each one verbatim to its matching Agent(prompt: ...):
77185
-
77186
- ${nativeInstructions.join("\n\n")}`;
77809
+ msg += nativeDispatchParallelHeader(nativeInstructions.length, parallelHost);
77810
+ msg += nativeInstructions.join("\n\n");
77187
77811
  msg += `
77188
77812
 
77189
77813
  \u26A0\uFE0F You MUST call gossip_relay for EVERY native agent after it completes. Without it, results are lost \u2014 no memory, no gossip, no consensus.`;
@@ -77382,15 +78006,16 @@ Task: ${def.task}`;
77382
78006
  const consensusInfo = ctx.nativeTaskMap.get(taskId);
77383
78007
  if (consensusInfo) consensusInfo.effectiveWriteMode = "sequential";
77384
78008
  }
77385
- const consensusWorktreeBanner = consensusUseWorktree ? `
77386
- Worktree isolation: REQUIRED \u2014 Agent() MUST be invoked with isolation: "worktree"` : "";
77387
- const consensusAgentCall = consensusUseWorktree ? `Agent(
77388
- model: "${nativeConfig.model}",
77389
- prompt: ${consensusPromptRef},
77390
- isolation: "worktree", // REQUIRED \u2014 do not omit
77391
- run_in_background: true
77392
- )` : `Agent(model: "${nativeConfig.model}", prompt: ${consensusPromptRef}, run_in_background: true)`;
77393
- lines.push(` ${taskId} \u2192 ${def.agent_id} (native \u2014 dispatch via Agent tool)`);
78009
+ const consensusHost2 = detectNativeHost();
78010
+ const consensusWorktreeBanner = nativeWorktreeBanner(consensusUseWorktree, consensusHost2);
78011
+ const consensusAgentCall = formatNativeAgentCall({
78012
+ agentId: def.agent_id,
78013
+ model: nativeConfig.model,
78014
+ promptRef: consensusPromptRef,
78015
+ useWorktree: consensusUseWorktree,
78016
+ host: consensusHost2
78017
+ });
78018
+ lines.push(` ${taskId} \u2192 ${def.agent_id} (native \u2014 dispatch via ${nativeDispatchViaLabel(consensusHost2)})`);
77394
78019
  nativeInstructions.push(
77395
78020
  `[${taskId}] ${consensusAgentCall}${consensusWorktreeBanner}
77396
78021
  \u2192 then: gossip_relay(task_id: "${taskId}", relay_token: "${relayToken}", result: "<output>")`
@@ -77406,8 +78031,10 @@ Task: ${def.task}`;
77406
78031
  }
77407
78032
  }
77408
78033
  stashDispatchWarnings(allTaskIds, dispatchRoundWarnings);
78034
+ const consensusHost = detectNativeHost();
78035
+ const nativeTool = nativeToolName(consensusHost);
77409
78036
  const collectCall = `gossip_collect(task_ids: [${allTaskIds.map((id) => `"${id}"`).join(", ")}], consensus: true)`;
77410
- let msg = `\u26A0\uFE0F REQUIRED_NEXT_ACTION: Agent() dispatch \u2014 this is a TODO, not a result.
78037
+ let msg = `\u26A0\uFE0F REQUIRED_NEXT_ACTION: ${nativeTool}() dispatch \u2014 this is a TODO, not a result.
77411
78038
  `;
77412
78039
  msg += `REQUIRED_NEXT: ${collectCall}
77413
78040
 
@@ -77420,23 +78047,19 @@ ${lines.join("\n")}`;
77420
78047
  `;
77421
78048
  msg += ` 1. \u2713 Phase 1 dispatched (task IDs above)
77422
78049
  `;
77423
- msg += ` 2. \u2192 Run native Agent() calls + relay each via gossip_relay(task_id, relay_token, result)
78050
+ msg += ` 2. \u2192 Run native ${nativeTool}() calls + relay each via gossip_relay(task_id, relay_token, result)
77424
78051
  `;
77425
78052
  msg += ` 3. \u2192 Call ${collectCall} \u2014 triggers PHASE 2 cross-review
77426
78053
  `;
77427
- msg += ` 4. \u2192 Run cross-review Agent() calls + relay each via gossip_relay_cross_review (DIFFERENT tool)
78054
+ msg += ` 4. \u2192 Run cross-review ${nativeTool}() calls + relay each via gossip_relay_cross_review (DIFFERENT tool)
77428
78055
  `;
77429
78056
  msg += ` 5. \u2192 Call gossip_collect(consensus: true) AGAIN for final synthesized output
77430
78057
  `;
77431
78058
  msg += `
77432
78059
  Stopping at step 2 produces fake-consensus results \u2014 agents never cross-validate each other's findings.`;
77433
78060
  if (nativeInstructions.length > 0) {
77434
- msg += `
77435
-
77436
- \u26A0\uFE0F NATIVE_DISPATCH \u2014 pass each AGENT_PROMPT content item VERBATIM to Agent(prompt: ...). Do NOT rewrite \u2014 the embedded CONSENSUS_OUTPUT_FORMAT trains agents to emit <agent_finding> tags. Call gossip_relay for EVERY native agent after completion.
77437
-
77438
- `;
77439
- msg += `Execute these ${nativeInstructions.length} Agent calls, then relay ALL results:
78061
+ msg += nativeDispatchConsensusFooter(consensusHost);
78062
+ msg += `Execute these ${nativeInstructions.length} ${nativeTool} calls, then relay ALL results:
77440
78063
 
77441
78064
  ${nativeInstructions.join("\n\n")}`;
77442
78065
  }
@@ -78535,19 +79158,19 @@ function filterWatchEvents(rawJsonl, opts) {
78535
79158
  }
78536
79159
 
78537
79160
  // apps/cli/src/stickyPort.ts
78538
- var import_fs81 = require("fs");
78539
- var import_path87 = require("path");
79161
+ var import_fs82 = require("fs");
79162
+ var import_path88 = require("path");
78540
79163
  var import_net = require("net");
78541
- var RELAY_STICKY_FILE = (0, import_path87.join)(".gossip", "relay.port");
78542
- var HTTP_MCP_STICKY_FILE = (0, import_path87.join)(".gossip", "http-mcp.port");
79164
+ var RELAY_STICKY_FILE = (0, import_path88.join)(".gossip", "relay.port");
79165
+ var HTTP_MCP_STICKY_FILE = (0, import_path88.join)(".gossip", "http-mcp.port");
78543
79166
  function stickyPath(filename) {
78544
- return (0, import_path87.join)(process.cwd(), filename);
79167
+ return (0, import_path88.join)(process.cwd(), filename);
78545
79168
  }
78546
79169
  function readStickyPort(filename) {
78547
79170
  const p = stickyPath(filename);
78548
- if (!(0, import_fs81.existsSync)(p)) return null;
79171
+ if (!(0, import_fs82.existsSync)(p)) return null;
78549
79172
  try {
78550
- const raw = (0, import_fs81.readFileSync)(p, "utf-8").trim();
79173
+ const raw = (0, import_fs82.readFileSync)(p, "utf-8").trim();
78551
79174
  const n = parseInt(raw, 10);
78552
79175
  if (!Number.isFinite(n) || n < 1 || n > 65535) return null;
78553
79176
  return n;
@@ -78559,8 +79182,8 @@ function writeStickyPort(filename, port) {
78559
79182
  if (!Number.isFinite(port) || port < 1 || port > 65535) return;
78560
79183
  const p = stickyPath(filename);
78561
79184
  try {
78562
- (0, import_fs81.mkdirSync)((0, import_path87.dirname)(p), { recursive: true });
78563
- (0, import_fs81.writeFileSync)(p, String(port), "utf-8");
79185
+ (0, import_fs82.mkdirSync)((0, import_path88.dirname)(p), { recursive: true });
79186
+ (0, import_fs82.writeFileSync)(p, String(port), "utf-8");
78564
79187
  } catch {
78565
79188
  }
78566
79189
  }
@@ -78641,12 +79264,12 @@ function flushStagedAgentFileWrites(writes, fs6) {
78641
79264
  }
78642
79265
 
78643
79266
  // apps/cli/src/native-agent-cache.ts
78644
- var import_fs82 = require("fs");
78645
- var import_path88 = require("path");
79267
+ var import_fs83 = require("fs");
79268
+ var import_path89 = require("path");
78646
79269
  var realFs = {
78647
- existsSync: import_fs82.existsSync,
78648
- readFileSync: import_fs82.readFileSync,
78649
- statSync: (p) => ({ mtimeMs: (0, import_fs82.statSync)(p).mtimeMs })
79270
+ existsSync: import_fs83.existsSync,
79271
+ readFileSync: import_fs83.readFileSync,
79272
+ statSync: (p) => ({ mtimeMs: (0, import_fs83.statSync)(p).mtimeMs })
78650
79273
  };
78651
79274
  function modelTierFromId(modelId) {
78652
79275
  if (modelId.includes("opus")) return "opus";
@@ -78655,8 +79278,8 @@ function modelTierFromId(modelId) {
78655
79278
  return "sonnet";
78656
79279
  }
78657
79280
  function refreshNativeAgentFromDisk(ac, cache3, projectRoot, fs6 = realFs) {
78658
- const claudeAgentPath = (0, import_path88.join)(projectRoot, ".claude", "agents", `${ac.id}.md`);
78659
- const instrPath = (0, import_path88.join)(projectRoot, ".gossip", "agents", ac.id, "instructions.md");
79281
+ const claudeAgentPath = (0, import_path89.join)(projectRoot, ".claude", "agents", `${ac.id}.md`);
79282
+ const instrPath = (0, import_path89.join)(projectRoot, ".gossip", "agents", ac.id, "instructions.md");
78660
79283
  const prev = cache3.get(ac.id);
78661
79284
  let sourcePath = null;
78662
79285
  let stripFrontmatter = false;
@@ -78715,25 +79338,35 @@ function refreshNativeAgentFromDisk(ac, cache3, projectRoot, fs6 = realFs) {
78715
79338
  }
78716
79339
 
78717
79340
  // apps/cli/src/rules-content.ts
78718
- var import_fs83 = require("fs");
78719
- var import_path89 = require("path");
79341
+ var import_fs84 = require("fs");
79342
+ var import_path90 = require("path");
78720
79343
  var AGENT_LIST_PLACEHOLDER = "{{AGENT_LIST}}";
78721
- function resolveRulesPath() {
79344
+ function resolveRulesPath(host = detectNativeHost()) {
79345
+ const fileName = host === "cursor" ? "CURSOR_RULES.md" : "RULES.md";
78722
79346
  const candidates = [
78723
- (0, import_path89.join)(process.cwd(), "docs", "RULES.md"),
78724
- (0, import_path89.join)(__dirname, "..", "docs", "RULES.md"),
78725
- (0, import_path89.join)(__dirname, "docs", "RULES.md")
79347
+ (0, import_path90.join)(process.cwd(), "docs", fileName),
79348
+ (0, import_path90.join)(__dirname, "..", "docs", fileName),
79349
+ (0, import_path90.join)(__dirname, "docs", fileName),
79350
+ // Fallback: bundled RULES.md when CURSOR_RULES.md not yet shipped
79351
+ ...host === "cursor" ? [
79352
+ (0, import_path90.join)(process.cwd(), "docs", "RULES.md"),
79353
+ (0, import_path90.join)(__dirname, "..", "docs", "RULES.md")
79354
+ ] : []
78726
79355
  ];
78727
- return candidates.find((p) => (0, import_fs83.existsSync)(p)) ?? null;
79356
+ return candidates.find((p) => (0, import_fs84.existsSync)(p)) ?? null;
78728
79357
  }
78729
- function generateRulesContent(agentList) {
78730
- const rulesPath = resolveRulesPath();
79358
+ function generateRulesContent(agentList, host) {
79359
+ const resolvedHost = host ?? detectNativeHost();
79360
+ const rulesPath = resolveRulesPath(resolvedHost);
78731
79361
  if (!rulesPath) {
78732
79362
  throw new Error(
78733
- `[gossipcat] generateRulesContent: docs/RULES.md not found in any fallback path (cwd=${process.cwd()}, dirname=${__dirname}). Expected docs/RULES.md tracked in the gossipcat repo or shipped alongside the MCP bundle. If running from a fresh install, re-run \`npm run build:mcp\` to copy docs/RULES.md into dist-mcp/docs/.`
79363
+ `[gossipcat] generateRulesContent: docs/RULES.md not found for host=${resolvedHost} (cwd=${process.cwd()}, dirname=${__dirname}). Expected docs/RULES.md or docs/CURSOR_RULES.md tracked in the gossipcat repo or shipped alongside the MCP bundle.`
78734
79364
  );
78735
79365
  }
78736
- const template = (0, import_fs83.readFileSync)(rulesPath, "utf-8");
79366
+ let template = (0, import_fs84.readFileSync)(rulesPath, "utf-8");
79367
+ if (resolvedHost === "cursor" && rulesPath.endsWith("RULES.md")) {
79368
+ template = template.replace(/Agent\(\)/g, "Task()").replace(/Agent\(/g, "Task(").replace(/Claude Code/g, "Cursor");
79369
+ }
78737
79370
  return template.split(AGENT_LIST_PLACEHOLDER).join(agentList);
78738
79371
  }
78739
79372
 
@@ -78920,12 +79553,12 @@ function readSecretFromStdin() {
78920
79553
  });
78921
79554
  }
78922
79555
  if (process.env.GOSSIPCAT_MCP_NO_MAIN !== "1" && !__argvShimHandled) {
78923
- const gossipDir = (0, import_path94.join)(process.cwd(), ".gossip");
79556
+ const gossipDir = (0, import_path95.join)(process.cwd(), ".gossip");
78924
79557
  try {
78925
- (0, import_fs88.mkdirSync)(gossipDir, { recursive: true });
79558
+ (0, import_fs89.mkdirSync)(gossipDir, { recursive: true });
78926
79559
  } catch {
78927
79560
  }
78928
- const logStream = (0, import_fs88.createWriteStream)((0, import_path94.join)(gossipDir, "mcp.log"), { flags: "a", mode: 384 });
79561
+ const logStream = (0, import_fs89.createWriteStream)((0, import_path95.join)(gossipDir, "mcp.log"), { flags: "a", mode: 384 });
78929
79562
  process.stderr.write = ((chunk, ...args) => {
78930
79563
  return logStream.write(chunk, ...args);
78931
79564
  });
@@ -78936,7 +79569,7 @@ try {
78936
79569
  } catch {
78937
79570
  }
78938
79571
  function memoryDirForProject(cwd) {
78939
- return (0, import_path94.join)((0, import_os8.homedir)(), ".claude", "projects", cwd.replaceAll("/", "-"), "memory");
79572
+ return (0, import_path95.join)((0, import_os8.homedir)(), ".claude", "projects", cwd.replaceAll("/", "-"), "memory");
78940
79573
  }
78941
79574
  function dashboardClickableUrl(url2, key) {
78942
79575
  if (!key) return url2;
@@ -78948,7 +79581,7 @@ function detectEnvironment() {
78948
79581
  return { host: "claude-code", supportsNativeAgents: true, nativeAgentDir: ".claude/agents", rulesDir: ".claude/rules", rulesFile: ".claude/rules/gossipcat.md" };
78949
79582
  }
78950
79583
  if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_SESSION_ID || process.env.CURSOR) {
78951
- return { host: "cursor", supportsNativeAgents: false, nativeAgentDir: null, rulesDir: ".cursor/rules", rulesFile: ".cursor/rules/gossipcat.mdc" };
79584
+ return { host: "cursor", supportsNativeAgents: true, nativeAgentDir: ".claude/agents", rulesDir: ".cursor/rules", rulesFile: ".cursor/rules/gossipcat.mdc" };
78952
79585
  }
78953
79586
  if (process.env.WINDSURF || process.env.WINDSURF_SESSION_ID) {
78954
79587
  return { host: "windsurf", supportsNativeAgents: false, nativeAgentDir: null, rulesDir: ".", rulesFile: ".windsurfrules" };
@@ -79011,14 +79644,14 @@ var _pendingPlanData = /* @__PURE__ */ new Map();
79011
79644
  var _utilityGuardSnapshots = /* @__PURE__ */ new Map();
79012
79645
  var _modules = null;
79013
79646
  function lookupFindingSeverity(findingId, projectRoot) {
79014
- const { existsSync: existsSync73, readdirSync: readdirSync22, readFileSync: readFileSync68 } = require("fs");
79015
- const { join: join90 } = require("path");
79016
- const reportsDir = join90(projectRoot, ".gossip", "consensus-reports");
79647
+ const { existsSync: existsSync73, readdirSync: readdirSync22, readFileSync: readFileSync69 } = require("fs");
79648
+ const { join: join91 } = require("path");
79649
+ const reportsDir = join91(projectRoot, ".gossip", "consensus-reports");
79017
79650
  if (!existsSync73(reportsDir)) return null;
79018
79651
  try {
79019
79652
  const files = readdirSync22(reportsDir).filter((f) => f.endsWith(".json"));
79020
79653
  for (const file2 of files) {
79021
- const report = JSON.parse(readFileSync68(join90(reportsDir, file2), "utf-8"));
79654
+ const report = JSON.parse(readFileSync69(join91(reportsDir, file2), "utf-8"));
79022
79655
  for (const bucket of ["confirmed", "disputed", "unverified", "unique"]) {
79023
79656
  for (const finding of report[bucket] || []) {
79024
79657
  if (finding.id === findingId && finding.severity) {
@@ -79117,7 +79750,7 @@ async function doBoot() {
79117
79750
  const agentConfigs = m.configToAgentConfigs(config2);
79118
79751
  ctx.keychain = new m.Keychain();
79119
79752
  const { existsSync: pidExists, readFileSync: readPid, writeFileSync: writePid, unlinkSync: delPid } = require("fs");
79120
- const pidFile = (0, import_path94.join)(process.cwd(), ".gossip", "relay.pid");
79753
+ const pidFile = (0, import_path95.join)(process.cwd(), ".gossip", "relay.pid");
79121
79754
  if (pidExists(pidFile)) {
79122
79755
  const oldPid = parseInt(readPid(pidFile, "utf-8").trim(), 10);
79123
79756
  if (!isNaN(oldPid) && oldPid !== process.pid) {
@@ -79239,10 +79872,10 @@ async function doBoot() {
79239
79872
  { id: ac.id, provider: ac.provider, model: ac.model, base_url: ac.base_url, key_ref: ac.key_ref },
79240
79873
  (s) => ctx.keychain.getKey(s)
79241
79874
  );
79242
- const { existsSync: existsSync73, readFileSync: readFileSync68 } = require("fs");
79243
- const { join: join90 } = require("path");
79244
- const instructionsPath = join90(process.cwd(), ".gossip", "agents", ac.id, "instructions.md");
79245
- const baseInstructions = existsSync73(instructionsPath) ? readFileSync68(instructionsPath, "utf-8") : "";
79875
+ const { existsSync: existsSync73, readFileSync: readFileSync69 } = require("fs");
79876
+ const { join: join91 } = require("path");
79877
+ const instructionsPath = join91(process.cwd(), ".gossip", "agents", ac.id, "instructions.md");
79878
+ const baseInstructions = existsSync73(instructionsPath) ? readFileSync69(instructionsPath, "utf-8") : "";
79246
79879
  const identity = ctx.identityRegistry.get(ac.id);
79247
79880
  const identityBlock = identity ? m.formatIdentityBlock(identity) + "\n" : "";
79248
79881
  const instructions = (identityBlock + baseInstructions).trim() || void 0;
@@ -79294,8 +79927,9 @@ async function doBoot() {
79294
79927
  if (!mainKey) {
79295
79928
  mainProvider = "none";
79296
79929
  config2.main_agent.provider = "none";
79297
- if (env.host === "claude-code") {
79298
- process.stderr.write(`[gossipcat] \u2705 Native Claude Code orchestration enabled (no API LLM needed \u2014 host classifies via natural language)
79930
+ if (env.host === "claude-code" || env.host === "cursor") {
79931
+ const hostLabel = env.host === "cursor" ? "Cursor" : "Claude Code";
79932
+ process.stderr.write(`[gossipcat] \u2705 Native ${hostLabel} orchestration enabled (no API LLM needed \u2014 host classifies via natural language)
79299
79933
  `);
79300
79934
  } else {
79301
79935
  process.stderr.write(`[gossipcat] \u274C No API keys available \u2014 orchestrator LLM disabled, features degrade to profile-based
@@ -80120,6 +80754,56 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
80120
80754
  return { content: [{ type: "text", text: `Sent reply to dashboard (chat_id ${chat_id}).` }] };
80121
80755
  }
80122
80756
  );
80757
+ server.tool(
80758
+ "gossip_ask",
80759
+ `Dashboard bridge ONLY: ask the dashboard chat (identified by chat_id) a structured selection question. The dashboard renders radios (single-select) or checkboxes (multiSelect) plus an optional free-text "Other"; the user's answer comes back to you as a NORMAL channel turn (a later message prefixed "[answer qid=\u2026]"). This is the dashboard-answerable parallel to the terminal-only AskUserQuestion \u2014 use it when a <channel source="gossipcat" chat_id="..."> dashboard chat is active. NON-BLOCKING: returns at once; do NOT wait for the answer in this call. Pass the chat_id verbatim. Returns an error (no side effect) when no matching dashboard bridge stream is open.`,
80760
+ {
80761
+ chat_id: external_exports.string().describe('The chat_id verbatim from the <channel ... chat_id="..."> dashboard message.'),
80762
+ questions: external_exports.array(
80763
+ external_exports.object({
80764
+ header: external_exports.string().describe('Short label for the question (e.g. "Approach").'),
80765
+ question: external_exports.string().describe("The prompt text shown above the options."),
80766
+ multiSelect: external_exports.boolean().optional().describe("true \u2192 checkboxes (multiple allowed); false/omitted \u2192 radios (one)."),
80767
+ options: external_exports.array(
80768
+ external_exports.object({
80769
+ label: external_exports.string().describe("The option label the user selects; must be unique within the question."),
80770
+ description: external_exports.string().optional().describe("Optional one-line clarification shown under the label.")
80771
+ })
80772
+ ).describe("Selectable options (\u22648)."),
80773
+ allowOther: external_exports.boolean().optional().describe('When true, the dashboard shows a free-text "Other" input.')
80774
+ })
80775
+ ).describe("1\u20134 questions, each with \u22648 options.")
80776
+ },
80777
+ async ({ chat_id, questions }) => {
80778
+ if (typeof chat_id !== "string" || chat_id.trim().length === 0) {
80779
+ return { content: [{ type: "text", text: "gossip_ask error: chat_id is required." }], isError: true };
80780
+ }
80781
+ if (!ctx.relay) {
80782
+ return { content: [{ type: "text", text: "gossip_ask error: no dashboard bridge is active (relay not started)." }], isError: true };
80783
+ }
80784
+ const { validateAskQuestions: validateAskQuestions2 } = await Promise.resolve().then(() => (init_src5(), src_exports4));
80785
+ const validated = validateAskQuestions2(questions);
80786
+ if ("error" in validated) {
80787
+ return { content: [{ type: "text", text: `gossip_ask error: ${validated.error}.` }], isError: true };
80788
+ }
80789
+ const qid = (0, import_crypto37.randomUUID)();
80790
+ let emitted = false;
80791
+ try {
80792
+ emitted = ctx.relay.emitBridgeQuestion(chat_id, qid, validated.questions) === true;
80793
+ } catch {
80794
+ emitted = false;
80795
+ }
80796
+ if (!emitted) {
80797
+ return {
80798
+ content: [{ type: "text", text: `gossip_ask error: no open dashboard bridge stream for chat_id "${chat_id}". This tool only sends to the dashboard channel.` }],
80799
+ isError: true
80800
+ };
80801
+ }
80802
+ return {
80803
+ content: [{ type: "text", text: `Asked the dashboard (qid=${qid}); the user's answer will arrive as a channel turn prefixed "[answer qid=${qid}]". Do not wait for it here.` }]
80804
+ };
80805
+ }
80806
+ );
80123
80807
  server.tool(
80124
80808
  "gossip_status",
80125
80809
  "Check Gossip Mesh system status, host environment, available agents, dashboard URL/key, and agent list with provider/model/skills. Pass slim:true to skip the handbook inline (~21KB savings) on reconnect refreshes.",
@@ -80177,7 +80861,7 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
80177
80861
  }
80178
80862
  try {
80179
80863
  const { readFileSync: rfHealth } = await import("fs");
80180
- const healthPath = (0, import_path94.join)(process.cwd(), ".gossip", "skill-runner-health.json");
80864
+ const healthPath = (0, import_path95.join)(process.cwd(), ".gossip", "skill-runner-health.json");
80181
80865
  const raw = rfHealth(healthPath, "utf8");
80182
80866
  const h = JSON.parse(raw);
80183
80867
  const lastMs = h.last_run_at ? new Date(h.last_run_at).getTime() : NaN;
@@ -80193,9 +80877,9 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
80193
80877
  lines.push(" Skill graduation: never run since session start (expected after first gossip_collect)");
80194
80878
  }
80195
80879
  try {
80196
- const { readFileSync: readFileSync68 } = await import("fs");
80197
- const quotaPath = (0, import_path94.join)(process.cwd(), ".gossip", "quota-state.json");
80198
- const quotaRaw = readFileSync68(quotaPath, "utf8");
80880
+ const { readFileSync: readFileSync69 } = await import("fs");
80881
+ const quotaPath = (0, import_path95.join)(process.cwd(), ".gossip", "quota-state.json");
80882
+ const quotaRaw = readFileSync69(quotaPath, "utf8");
80199
80883
  const quotaState = JSON.parse(quotaRaw);
80200
80884
  for (const [provider, state] of Object.entries(quotaState)) {
80201
80885
  const now = Date.now();
@@ -80247,7 +80931,7 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
80247
80931
  }
80248
80932
  try {
80249
80933
  const { existsSync: hookExists } = await import("fs");
80250
- const hookScriptPath = (0, import_path94.join)(process.cwd(), ".claude", "hooks", "worktree-sandbox.sh");
80934
+ const hookScriptPath = (0, import_path95.join)(process.cwd(), ".claude", "hooks", "worktree-sandbox.sh");
80251
80935
  const { isWorktreeSandboxHookRegistered: isWorktreeSandboxHookRegistered2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
80252
80936
  if (hookExists(hookScriptPath) && !isWorktreeSandboxHookRegistered2(process.cwd())) {
80253
80937
  lines.push(" \u26A0\uFE0F Sandbox: worktree-sandbox hook installed but UNREGISTERED \u2014 run gossip_setup to re-register");
@@ -80255,21 +80939,21 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
80255
80939
  } catch {
80256
80940
  }
80257
80941
  try {
80258
- const { readdirSync: readdirSync22, statSync: statSync36, readFileSync: readFileSync68 } = await import("fs");
80942
+ const { readdirSync: readdirSync22, statSync: statSync36, readFileSync: readFileSync69 } = await import("fs");
80259
80943
  const { readJsonlWithRotated: readJsonlRotated1506 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
80260
- const reportsDir = (0, import_path94.join)(process.cwd(), ".gossip", "consensus-reports");
80261
- const perfPath = (0, import_path94.join)(process.cwd(), ".gossip", "agent-performance.jsonl");
80944
+ const reportsDir = (0, import_path95.join)(process.cwd(), ".gossip", "consensus-reports");
80945
+ const perfPath = (0, import_path95.join)(process.cwd(), ".gossip", "agent-performance.jsonl");
80262
80946
  const WINDOW_MS3 = 24 * 60 * 60 * 1e3;
80263
80947
  const now = Date.now();
80264
80948
  const recentReports = [];
80265
80949
  try {
80266
80950
  for (const fname of readdirSync22(reportsDir)) {
80267
80951
  if (!fname.endsWith(".json")) continue;
80268
- const fpath = (0, import_path94.join)(reportsDir, fname);
80952
+ const fpath = (0, import_path95.join)(reportsDir, fname);
80269
80953
  const st = statSync36(fpath);
80270
80954
  if (now - st.mtimeMs > WINDOW_MS3) continue;
80271
80955
  try {
80272
- const report = JSON.parse(readFileSync68(fpath, "utf8"));
80956
+ const report = JSON.parse(readFileSync69(fpath, "utf8"));
80273
80957
  const isEmpty = (report.confirmed?.length ?? 0) === 0 && (report.disputed?.length ?? 0) === 0 && (report.unverified?.length ?? 0) === 0 && (report.unique?.length ?? 0) === 0 && (report.newFindings?.length ?? 0) === 0 && (report.insights?.length ?? 0) === 0;
80274
80958
  if (isEmpty) continue;
80275
80959
  } catch {
@@ -80643,8 +81327,8 @@ ${body}`
80643
81327
  }
80644
81328
  return { content: [{ type: "text", text: results.join("\n") }] };
80645
81329
  }
80646
- const { writeFileSync: writeFileSync37, mkdirSync: mkdirSync44, existsSync: existsSync73 } = require("fs");
80647
- const { join: join90 } = require("path");
81330
+ const { writeFileSync: writeFileSync38, mkdirSync: mkdirSync45, existsSync: existsSync73 } = require("fs");
81331
+ const { join: join91 } = require("path");
80648
81332
  const root = process.cwd();
80649
81333
  const CLAUDE_MODEL_MAP2 = {
80650
81334
  opus: { provider: "anthropic", model: "claude-opus-4-6" },
@@ -80660,8 +81344,8 @@ ${body}`
80660
81344
  let existingConfig = {};
80661
81345
  let existingAgents = {};
80662
81346
  try {
80663
- const { readFileSync: readFileSync68 } = require("fs");
80664
- const existing = JSON.parse(readFileSync68(join90(root, ".gossip", "config.json"), "utf-8"));
81347
+ const { readFileSync: readFileSync69 } = require("fs");
81348
+ const existing = JSON.parse(readFileSync69(join91(root, ".gossip", "config.json"), "utf-8"));
80665
81349
  if (existing && typeof existing === "object" && !Array.isArray(existing)) {
80666
81350
  existingConfig = existing;
80667
81351
  if (mode === "merge") existingAgents = existing.agents || {};
@@ -80695,8 +81379,8 @@ ${body}`
80695
81379
  body
80696
81380
  ].join("\n");
80697
81381
  pendingAgentFileWrites.push({
80698
- dir: join90(root, ".claude", "agents"),
80699
- path: join90(root, ".claude", "agents", `${agent.id}.md`),
81382
+ dir: join91(root, ".claude", "agents"),
81383
+ path: join91(root, ".claude", "agents", `${agent.id}.md`),
80700
81384
  content: md
80701
81385
  });
80702
81386
  nativeCreated.push(agent.id);
@@ -80717,7 +81401,7 @@ ${body}`
80717
81401
  errors.push(`${agent.id}: custom agent requires "custom_model" field`);
80718
81402
  continue;
80719
81403
  }
80720
- const nativeFile = join90(root, ".claude", "agents", `${agent.id}.md`);
81404
+ const nativeFile = join91(root, ".claude", "agents", `${agent.id}.md`);
80721
81405
  const wasNative = existingAgents[agent.id]?.native || configAgents[agent.id]?.native || existsSync73(nativeFile);
80722
81406
  if (wasNative) {
80723
81407
  errors.push(`${agent.id}: cannot re-register native agent as custom \u2014 .claude/agents/${agent.id}.md exists. Remove the file first or keep it as native.`);
@@ -80733,10 +81417,10 @@ ${body}`
80733
81417
  };
80734
81418
  customCreated.push(agent.id);
80735
81419
  if (agent.instructions) {
80736
- const instrDir = join90(root, ".gossip", "agents", agent.id);
81420
+ const instrDir = join91(root, ".gossip", "agents", agent.id);
80737
81421
  pendingAgentFileWrites.push({
80738
81422
  dir: instrDir,
80739
- path: join90(instrDir, "instructions.md"),
81423
+ path: join91(instrDir, "instructions.md"),
80740
81424
  content: agent.instructions
80741
81425
  });
80742
81426
  }
@@ -80754,9 +81438,9 @@ ${body}`
80754
81438
  } catch (err) {
80755
81439
  return { content: [{ type: "text", text: `Invalid config: ${err.message}` }] };
80756
81440
  }
80757
- flushStagedAgentFileWrites(pendingAgentFileWrites, { mkdirSync: mkdirSync44, writeFileSync: writeFileSync37 });
80758
- mkdirSync44(join90(root, ".gossip"), { recursive: true });
80759
- writeFileSync37(join90(root, ".gossip", "config.json"), JSON.stringify(config2, null, 2));
81441
+ flushStagedAgentFileWrites(pendingAgentFileWrites, { mkdirSync: mkdirSync45, writeFileSync: writeFileSync38 });
81442
+ mkdirSync45(join91(root, ".gossip"), { recursive: true });
81443
+ writeFileSync38(join91(root, ".gossip", "config.json"), JSON.stringify(config2, null, 2));
80760
81444
  let hookSummary = "";
80761
81445
  try {
80762
81446
  const { installWorktreeSandboxHook: installWorktreeSandboxHook2, writeOrchestratorRoleMarker: writeOrchestratorRoleMarker2 } = (init_src4(), __toCommonJS(src_exports3));
@@ -80859,10 +81543,13 @@ ${body}`
80859
81543
  }
80860
81544
  }
80861
81545
  const agentList = Object.entries(config2.agents).map(([id, a]) => `- ${id}: ${a.provider}/${a.model} (${a.preset || "custom"})${a.native ? " \u2014 native" : ""}`).join("\n");
80862
- const rulesDir = join90(root, env.rulesDir);
80863
- const rulesFile = join90(root, env.rulesFile);
80864
- mkdirSync44(rulesDir, { recursive: true });
80865
- writeFileSync37(rulesFile, generateRulesContent(agentList));
81546
+ const rulesDir = join91(root, env.rulesDir);
81547
+ const rulesFile = join91(root, env.rulesFile);
81548
+ mkdirSync45(rulesDir, { recursive: true });
81549
+ writeFileSync38(rulesFile, generateRulesContent(
81550
+ agentList,
81551
+ env.host === "cursor" || env.host === "claude-code" ? env.host : void 0
81552
+ ));
80866
81553
  const lines = [`Host: ${env.host}`, ""];
80867
81554
  if (nativeCreated.length > 0) {
80868
81555
  lines.push(`Native agents created (${nativeCreated.length}):`);
@@ -80948,8 +81635,8 @@ Tip: Native agents may prompt for file write permissions. To auto-allow, add to
80948
81635
  if (agent_id === "auto") {
80949
81636
  try {
80950
81637
  await syncWorkersViaKeychain();
80951
- const isNullLlm = ctx.mainProvider === "none";
80952
- if (isNullLlm && env.host === "claude-code") {
81638
+ const isNullLlm = ctx.mainProvider === "none" || ctx.mainProviderConfig === "none";
81639
+ if (isNullLlm && (env.host === "claude-code" || env.host === "cursor")) {
80953
81640
  const agents = ctx.mainAgent.getAgentList?.() ?? [];
80954
81641
  const agentSummary = agents.map(
80955
81642
  (a) => `- ${a.id} (${a.provider}/${a.model}) [${a.skills?.join(", ") || "no skills"}]`
@@ -81166,11 +81853,11 @@ The original signal remains in the audit log but will be excluded from scoring.`
81166
81853
  const fid = finding_id.trim();
81167
81854
  const cwd = process.cwd();
81168
81855
  const findingsPath = require("path").join(cwd, ".gossip", "implementation-findings.jsonl");
81169
- const { readFileSync: readFileSync68, existsSync: existsSync73, writeFileSync: writeFileSync37, renameSync: renameSync17 } = require("fs");
81856
+ const { readFileSync: readFileSync69, existsSync: existsSync73, writeFileSync: writeFileSync38, renameSync: renameSync18 } = require("fs");
81170
81857
  if (!existsSync73(findingsPath)) {
81171
81858
  return { content: [{ type: "text", text: `No implementation-findings.jsonl found at ${findingsPath}` }] };
81172
81859
  }
81173
- const lines = readFileSync68(findingsPath, "utf-8").split("\n");
81860
+ const lines = readFileSync69(findingsPath, "utf-8").split("\n");
81174
81861
  let matched = false;
81175
81862
  let alreadyTarget = false;
81176
81863
  let matchedEntry = null;
@@ -81226,8 +81913,8 @@ The original signal remains in the audit log but will be excluded from scoring.`
81226
81913
  }
81227
81914
  try {
81228
81915
  const tmp = findingsPath + ".tmp." + Date.now();
81229
- writeFileSync37(tmp, newLines.join("\n"));
81230
- renameSync17(tmp, findingsPath);
81916
+ writeFileSync38(tmp, newLines.join("\n"));
81917
+ renameSync18(tmp, findingsPath);
81231
81918
  } catch (err) {
81232
81919
  return { content: [{ type: "text", text: `Failed to write findings: ${err.message}` }] };
81233
81920
  }
@@ -81258,18 +81945,18 @@ The original signal remains in the audit log but will be excluded from scoring.`
81258
81945
  return { content: [{ type: "text", text: "Error: consensus_id is required for bulk_from_consensus." }] };
81259
81946
  }
81260
81947
  try {
81261
- const { readFileSync: readFileSync68 } = await import("fs");
81262
- const { join: join90 } = await import("path");
81263
- const reportPath = join90(process.cwd(), ".gossip", "consensus-reports", `${consensus_id}.json`);
81948
+ const { readFileSync: readFileSync69 } = await import("fs");
81949
+ const { join: join91 } = await import("path");
81950
+ const reportPath = join91(process.cwd(), ".gossip", "consensus-reports", `${consensus_id}.json`);
81264
81951
  let report;
81265
81952
  try {
81266
- report = JSON.parse(readFileSync68(reportPath, "utf-8"));
81953
+ report = JSON.parse(readFileSync69(reportPath, "utf-8"));
81267
81954
  } catch {
81268
81955
  return { content: [{ type: "text", text: `Error: consensus report not found: ${consensus_id}` }] };
81269
81956
  }
81270
81957
  const existingFindingIds = /* @__PURE__ */ new Set();
81271
81958
  try {
81272
- const perfPath = join90(process.cwd(), ".gossip", "agent-performance.jsonl");
81959
+ const perfPath = join91(process.cwd(), ".gossip", "agent-performance.jsonl");
81273
81960
  const { readJsonlWithRotated: readJsonlRotated2570 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
81274
81961
  const lines = readJsonlRotated2570(perfPath).split("\n").filter(Boolean);
81275
81962
  for (const line of lines) {
@@ -82317,16 +83004,16 @@ ${preview}` }]
82317
83004
  const { SkillGapTracker: SkillGapTracker2, parseSkillFrontmatter: parseSkillFrontmatter2, normalizeSkillName: normalizeSkillName2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
82318
83005
  const tracker = new SkillGapTracker2(process.cwd());
82319
83006
  if (skills && skills.length > 0) {
82320
- const { writeFileSync: writeFileSync37, mkdirSync: mkdirSync44, existsSync: existsSync73, readFileSync: readFileSync68 } = require("fs");
82321
- const { join: join90 } = require("path");
82322
- const dir = join90(process.cwd(), ".gossip", "skills");
82323
- mkdirSync44(dir, { recursive: true });
83007
+ const { writeFileSync: writeFileSync38, mkdirSync: mkdirSync45, existsSync: existsSync73, readFileSync: readFileSync69 } = require("fs");
83008
+ const { join: join91 } = require("path");
83009
+ const dir = join91(process.cwd(), ".gossip", "skills");
83010
+ mkdirSync45(dir, { recursive: true });
82324
83011
  const results = [];
82325
83012
  for (const sk of skills) {
82326
83013
  const name = normalizeSkillName2(sk.name);
82327
- const filePath = join90(dir, `${name}.md`);
83014
+ const filePath = join91(dir, `${name}.md`);
82328
83015
  if (existsSync73(filePath)) {
82329
- const existing = readFileSync68(filePath, "utf-8");
83016
+ const existing = readFileSync69(filePath, "utf-8");
82330
83017
  const fm = parseSkillFrontmatter2(existing);
82331
83018
  if (fm) {
82332
83019
  if (fm.generated_by === "manual") {
@@ -82343,7 +83030,7 @@ ${preview}` }]
82343
83030
  }
82344
83031
  }
82345
83032
  }
82346
- writeFileSync37(filePath, sk.content);
83033
+ writeFileSync38(filePath, sk.content);
82347
83034
  tracker.recordResolution(name);
82348
83035
  results.push(`Created .gossip/skills/${name}.md`);
82349
83036
  }
@@ -83166,9 +83853,9 @@ ${p.user}
83166
83853
  max_events: external_exports.number().int().positive().max(WATCH_MAX_EVENTS).optional().describe(`Cap events returned (default ${WATCH_MAX_EVENTS}).`)
83167
83854
  },
83168
83855
  async ({ cursor, max_events }) => {
83169
- const { join: join90 } = await import("node:path");
83856
+ const { join: join91 } = await import("node:path");
83170
83857
  const { readJsonlWithRotated: readJsonlWithRotated2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
83171
- const perfPath = join90(process.cwd(), ".gossip", "agent-performance.jsonl");
83858
+ const perfPath = join91(process.cwd(), ".gossip", "agent-performance.jsonl");
83172
83859
  const raw = readJsonlWithRotated2(perfPath);
83173
83860
  const result = filterWatchEvents(raw, { cursor, maxEvents: max_events });
83174
83861
  return { content: [{ type: "text", text: JSON.stringify(result) }] };