gossipcat 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,9 @@ var init_mcp_context = __esm({
55
55
  pendingConsensusRounds: /* @__PURE__ */ new Map(),
56
56
  nativeUtilityConfig: null,
57
57
  mainProvider: "google",
58
+ httpMcpPort: null,
59
+ relayPortSource: null,
60
+ httpMcpPortSource: null,
58
61
  booted: false,
59
62
  boot: async () => {
60
63
  throw new Error("boot not initialized");
@@ -9219,6 +9222,17 @@ function loadSkills(agentId, skills, projectRoot, index, task) {
9219
9222
  for (const skill of effectiveSkills) {
9220
9223
  const content = resolveSkill(agentId, skill, projectRoot);
9221
9224
  if (!content) continue;
9225
+ const frontmatterStatus = parseSkillFrontmatter(content)?.status;
9226
+ if (frontmatterStatus === "failed" || frontmatterStatus === "silent_skill") {
9227
+ process.stderr.write(`[gossipcat] Skipping ${frontmatterStatus} skill ${agentId}/${skill} from injection
9228
+ `);
9229
+ dropped.push(skill);
9230
+ continue;
9231
+ }
9232
+ if (frontmatterStatus === "flagged_for_manual_review") {
9233
+ process.stderr.write(`[gossipcat] Injecting flagged_for_manual_review skill ${agentId}/${skill} \u2014 manual review recommended
9234
+ `);
9235
+ }
9222
9236
  const mode = index?.getSkillMode(agentId, skill) ?? "permanent";
9223
9237
  if (mode === "permanent") {
9224
9238
  permanent.push({ name: skill, content });
@@ -11310,6 +11324,7 @@ var init_performance_reader = __esm({
11310
11324
  "use strict";
11311
11325
  import_fs15 = require("fs");
11312
11326
  import_path17 = require("path");
11327
+ init_skill_name();
11313
11328
  CIRCUIT_BREAKER_THRESHOLD = 3;
11314
11329
  NEGATIVE_SIGNALS = /* @__PURE__ */ new Set(["hallucination_caught", "disagreement", "unique_unconfirmed"]);
11315
11330
  SIGNAL_EXPIRY_DAYS = 30;
@@ -11393,9 +11408,10 @@ var init_performance_reader = __esm({
11393
11408
  getCountersSince(agentId, category, sinceMs) {
11394
11409
  const allSignals = this.readSignalsRaw();
11395
11410
  const counters = { correct: 0, hallucinated: 0 };
11411
+ const normalizedTarget = normalizeSkillName(category);
11396
11412
  for (const s of allSignals) {
11397
11413
  if (s.agentId !== agentId) continue;
11398
- if (s.category !== category) continue;
11414
+ if (normalizeSkillName(s.category ?? "") !== normalizedTarget) continue;
11399
11415
  const ts = s.timestamp ? new Date(s.timestamp).getTime() : 0;
11400
11416
  if (!isFinite(ts) || ts === 0 || ts < sinceMs) continue;
11401
11417
  switch (s.signal) {
@@ -11670,6 +11686,7 @@ var init_performance_reader = __esm({
11670
11686
  const timeFreshness = Math.pow(0.5, daysSinceLastSignal / halfLife);
11671
11687
  reliability = 0.5 + (reliability - 0.5) * timeFreshness;
11672
11688
  }
11689
+ const MIN_CATEGORY_N = 5;
11673
11690
  const categoryAccuracy = {};
11674
11691
  const allCategories = /* @__PURE__ */ new Set([
11675
11692
  ...Object.keys(a.categoryCorrect),
@@ -11678,7 +11695,7 @@ var init_performance_reader = __esm({
11678
11695
  for (const cat of allCategories) {
11679
11696
  const c = a.categoryCorrect[cat] ?? 0;
11680
11697
  const h = a.categoryHallucinated[cat] ?? 0;
11681
- if (c + h > 0) categoryAccuracy[cat] = c / (c + h);
11698
+ if (c + h >= MIN_CATEGORY_N) categoryAccuracy[cat] = c / (c + h);
11682
11699
  }
11683
11700
  const consec = consecutiveFailures.get(id) || 0;
11684
11701
  scores.set(id, {
@@ -15588,8 +15605,8 @@ Keep it SHORT \u2014 under 30 lines. This is a working document, not a design do
15588
15605
  ]);
15589
15606
  const specContent = response.text || "";
15590
15607
  try {
15591
- const { mkdirSync: mkdirSync21, writeFileSync: writeFS } = require("fs");
15592
- mkdirSync21((0, import_path23.join)(this.projectRoot, ".gossip"), { recursive: true });
15608
+ const { mkdirSync: mkdirSync22, writeFileSync: writeFS } = require("fs");
15609
+ mkdirSync22((0, import_path23.join)(this.projectRoot, ".gossip"), { recursive: true });
15593
15610
  writeFS(specPath, specContent, "utf-8");
15594
15611
  } catch (err) {
15595
15612
  return { text: `Spec generated but failed to save: ${err.message}
@@ -16424,8 +16441,8 @@ message: Your question?
16424
16441
  }
16425
16442
  /** Start all worker agents (connect to relay) */
16426
16443
  async start() {
16427
- const { existsSync: existsSync43, readFileSync: readFileSync40 } = await import("fs");
16428
- const { join: join48 } = await import("path");
16444
+ const { existsSync: existsSync44, readFileSync: readFileSync41 } = await import("fs");
16445
+ const { join: join49 } = await import("path");
16429
16446
  for (const config2 of this.registry.getAll()) {
16430
16447
  if (config2.native) continue;
16431
16448
  if (this.workers.has(config2.id)) continue;
@@ -16434,8 +16451,8 @@ message: Your question?
16434
16451
  apiKey = await this.keyProviderFn(config2.provider) ?? void 0;
16435
16452
  }
16436
16453
  const llm = createProvider(config2.provider, config2.model, apiKey);
16437
- const instructionsPath = join48(this.projectRoot, ".gossip", "agents", config2.id, "instructions.md");
16438
- const instructions = existsSync43(instructionsPath) ? readFileSync40(instructionsPath, "utf-8") : void 0;
16454
+ const instructionsPath = join49(this.projectRoot, ".gossip", "agents", config2.id, "instructions.md");
16455
+ const instructions = existsSync44(instructionsPath) ? readFileSync41(instructionsPath, "utf-8") : void 0;
16439
16456
  const enableWebSearch = config2.preset === "researcher" || config2.skills.includes("research");
16440
16457
  const worker = new WorkerAgent(config2.id, llm, this.relayUrl, ALL_TOOLS, instructions, enableWebSearch, this.relayApiKey);
16441
16458
  await worker.start();
@@ -16617,16 +16634,16 @@ message: Your question?
16617
16634
  this.registry.register(config2);
16618
16635
  }
16619
16636
  async syncWorkers(keyProvider) {
16620
- const { existsSync: existsSync43, readFileSync: readFileSync40 } = await import("fs");
16621
- const { join: join48 } = await import("path");
16637
+ const { existsSync: existsSync44, readFileSync: readFileSync41 } = await import("fs");
16638
+ const { join: join49 } = await import("path");
16622
16639
  let added = 0;
16623
16640
  for (const ac of this.registry.getAll()) {
16624
16641
  if (ac.native) continue;
16625
16642
  if (this.workers.has(ac.id)) continue;
16626
16643
  const key = await keyProvider(ac.provider);
16627
16644
  const llm = createProvider(ac.provider, ac.model, key ?? void 0, void 0, ac.base_url);
16628
- const instructionsPath = join48(this.projectRoot, ".gossip", "agents", ac.id, "instructions.md");
16629
- const instructions = existsSync43(instructionsPath) ? readFileSync40(instructionsPath, "utf-8") : void 0;
16645
+ const instructionsPath = join49(this.projectRoot, ".gossip", "agents", ac.id, "instructions.md");
16646
+ const instructions = existsSync44(instructionsPath) ? readFileSync41(instructionsPath, "utf-8") : void 0;
16630
16647
  const enableWebSearch = ac.preset === "researcher" || ac.skills.includes("research");
16631
16648
  const worker = new WorkerAgent(ac.id, llm, this.relayUrl, ALL_TOOLS, instructions, enableWebSearch, this.relayApiKey);
16632
16649
  await worker.start();
@@ -18398,7 +18415,13 @@ NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.
18398
18415
  this.projectRoot = projectRoot;
18399
18416
  }
18400
18417
  techStackCache = void 0;
18401
- async generate(agentId, category) {
18418
+ /**
18419
+ * Build the LLM prompt strings and metadata needed to generate a skill file.
18420
+ * Separated from generate() so callers (e.g. the native-utility re-entry path)
18421
+ * can obtain the prompt, dispatch it externally, and later call saveFromRaw()
18422
+ * with the result — without re-running the expensive data-gathering step.
18423
+ */
18424
+ async buildPrompt(agentId, category) {
18402
18425
  if (!SAFE_NAME.test(agentId)) {
18403
18426
  throw new Error(`Invalid agent_id: "${agentId}". Must be lowercase alphanumeric with hyphens/underscores.`);
18404
18427
  }
@@ -18437,10 +18460,13 @@ ${techStack}
18437
18460
  const totalDispatches = agentScoreData?.totalSignals ?? 0;
18438
18461
  const categoryConfirmations = findings.filter((f) => f.agentId === agentId).length;
18439
18462
  const baselineRate = totalDispatches > 0 ? categoryConfirmations / totalDispatches : 0;
18440
- const messages = [
18441
- {
18442
- role: "system",
18443
- content: `You are a senior prompt engineer who builds skill files for AI code review agents. Your skills are injected into agent system prompts at dispatch time \u2014 every word costs tokens and shapes behavior. You write concise, opinionated methodology that changes how an agent thinks about a specific class of problems.
18463
+ const lifetime = this.perfReader.getCountersSince(agentId, category, 0);
18464
+ const baseline_accuracy_correct = lifetime.correct;
18465
+ const baseline_accuracy_hallucinated = lifetime.hallucinated;
18466
+ const bound_at = (/* @__PURE__ */ new Date()).toISOString();
18467
+ const skillName = normalizeSkillName(category);
18468
+ const skillPath = (0, import_path31.join)(this.projectRoot, ".gossip", "agents", agentId, "skills", `${skillName}.md`);
18469
+ const system = `You are a senior prompt engineer who builds skill files for AI code review agents. Your skills are injected into agent system prompts at dispatch time \u2014 every word costs tokens and shapes behavior. You write concise, opinionated methodology that changes how an agent thinks about a specific class of problems.
18444
18470
 
18445
18471
  Your output quality is measured by:
18446
18472
  1. **Relevance** \u2014 every check must apply to THIS project's tech stack. Generic checklists are waste.
@@ -18452,11 +18478,8 @@ Study this reference skill \u2014 it represents the quality bar:
18452
18478
 
18453
18479
  <reference_skill>
18454
18480
  ${template}
18455
- </reference_skill>`
18456
- },
18457
- {
18458
- role: "user",
18459
- content: `Generate a skill file for agent "${agentId}" to improve its "${category}" review performance.
18481
+ </reference_skill>`;
18482
+ const user = `Generate a skill file for agent "${agentId}" to improve its "${category}" review performance.
18460
18483
 
18461
18484
  <project_context>
18462
18485
  ${projectContext || "No project context available."}
@@ -18487,27 +18510,42 @@ Requirements:
18487
18510
  - Keep under 150 lines
18488
18511
  - CRITICAL: Tailor ALL content to the project's actual tech stack (see <tech_stack>). Only include checks relevant to technologies the project uses. If the project has no SQL database, do NOT mention SQL injection. If no HTML rendering, do NOT mention XSS. Generic security checklists waste agent prompt tokens.
18489
18512
  - Reference actual project file paths and patterns from findings and context
18490
- - Use bullet lists instead of markdown tables for Anti-Patterns (tables render poorly in agent prompts)`
18491
- }
18492
- ];
18493
- const response = await this.llm.generate(messages, { temperature: 0.3 });
18494
- const content = response.text || "";
18495
- let cleaned = content.trim();
18513
+ - Use bullet lists instead of markdown tables for Anti-Patterns (tables render poorly in agent prompts)`;
18514
+ return { system, user, skillName, skillPath, baseline_accuracy_correct, baseline_accuracy_hallucinated, bound_at };
18515
+ }
18516
+ /**
18517
+ * Persist a raw LLM-generated skill markdown string to disk.
18518
+ * Mirrors the post-LLM steps from generate(): code-fence stripping,
18519
+ * validation, snapshot-field injection, and atomic file write.
18520
+ *
18521
+ * Called by the native-utility re-entry path after gossip_relay delivers
18522
+ * the agent output back to the orchestrator.
18523
+ */
18524
+ saveFromRaw(_agentId, _category, rawMarkdown, meta3) {
18525
+ let cleaned = rawMarkdown.trim();
18496
18526
  if (cleaned.startsWith("```")) {
18497
18527
  cleaned = cleaned.replace(/^```\w*\n/, "").replace(/\n```\s*$/, "").trim();
18498
18528
  }
18499
18529
  this.validateSkillContent(cleaned);
18500
- const lifetime = this.perfReader.getCountersSince(agentId, category, 0);
18501
- const baseline_accuracy_correct = lifetime.correct;
18502
- const baseline_accuracy_hallucinated = lifetime.hallucinated;
18503
- const bound_at = (/* @__PURE__ */ new Date()).toISOString();
18504
- cleaned = this.injectSnapshotFields(cleaned, { baseline_accuracy_correct, baseline_accuracy_hallucinated, bound_at });
18505
- const skillName = normalizeSkillName(category);
18506
- const skillDir = (0, import_path31.join)(this.projectRoot, ".gossip", "agents", agentId, "skills");
18530
+ cleaned = this.injectSnapshotFields(cleaned, {
18531
+ baseline_accuracy_correct: meta3.baseline_accuracy_correct,
18532
+ baseline_accuracy_hallucinated: meta3.baseline_accuracy_hallucinated,
18533
+ bound_at: meta3.bound_at
18534
+ });
18535
+ const skillDir = (0, import_path31.join)(this.projectRoot, ".gossip", "agents", _agentId, "skills");
18507
18536
  (0, import_fs29.mkdirSync)(skillDir, { recursive: true });
18508
- const skillPath = (0, import_path31.join)(skillDir, `${skillName}.md`);
18509
- (0, import_fs29.writeFileSync)(skillPath, cleaned);
18510
- return { path: skillPath, content: cleaned };
18537
+ (0, import_fs29.writeFileSync)(meta3.skillPath, cleaned);
18538
+ return { path: meta3.skillPath, content: cleaned };
18539
+ }
18540
+ async generate(agentId, category) {
18541
+ const promptData = await this.buildPrompt(agentId, category);
18542
+ const messages = [
18543
+ { role: "system", content: promptData.system },
18544
+ { role: "user", content: promptData.user }
18545
+ ];
18546
+ const response = await this.llm.generate(messages, { temperature: 0.3 });
18547
+ const content = response.text || "";
18548
+ return this.saveFromRaw(agentId, category, content, promptData);
18511
18549
  }
18512
18550
  /**
18513
18551
  * Post-processes LLM-generated skill content to inject or overwrite snapshot fields
@@ -19425,11 +19463,11 @@ function startConsensusTimeout(consensusId) {
19425
19463
  const allEntries = [...snapshot.relayCrossReviewEntries, ...snapshot.nativeCrossReviewEntries];
19426
19464
  const report = await engine.synthesizeWithCrossReview(snapshot.allResults, allEntries, consensusId, snapshot.relayCrossReviewSkipped);
19427
19465
  try {
19428
- const { writeFileSync: writeFileSync17, mkdirSync: mkdirSync21 } = require("fs");
19429
- const { join: join48 } = require("path");
19430
- const reportsDir = join48(process.cwd(), ".gossip", "consensus-reports");
19431
- mkdirSync21(reportsDir, { recursive: true });
19432
- writeFileSync17(join48(reportsDir, `${consensusId}.json`), JSON.stringify({
19466
+ const { writeFileSync: writeFileSync18, mkdirSync: mkdirSync22 } = require("fs");
19467
+ const { join: join49 } = require("path");
19468
+ const reportsDir = join49(process.cwd(), ".gossip", "consensus-reports");
19469
+ mkdirSync22(reportsDir, { recursive: true });
19470
+ writeFileSync18(join49(reportsDir, `${consensusId}.json`), JSON.stringify({
19433
19471
  id: consensusId,
19434
19472
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19435
19473
  agentCount: report.agentCount,
@@ -19577,12 +19615,12 @@ async function handleRelayCrossReview(consensus_id, agent_id, result) {
19577
19615
  synthSnapshot.relayCrossReviewSkipped
19578
19616
  );
19579
19617
  try {
19580
- const { writeFileSync: writeFileSync17, mkdirSync: mkdirSync21 } = require("fs");
19581
- const { join: join48 } = require("path");
19582
- const reportsDir = join48(process.cwd(), ".gossip", "consensus-reports");
19583
- mkdirSync21(reportsDir, { recursive: true });
19584
- const reportPath = join48(reportsDir, `${consensus_id}.json`);
19585
- writeFileSync17(reportPath, JSON.stringify({
19618
+ const { writeFileSync: writeFileSync18, mkdirSync: mkdirSync22 } = require("fs");
19619
+ const { join: join49 } = require("path");
19620
+ const reportsDir = join49(process.cwd(), ".gossip", "consensus-reports");
19621
+ mkdirSync22(reportsDir, { recursive: true });
19622
+ const reportPath = join49(reportsDir, `${consensus_id}.json`);
19623
+ writeFileSync18(reportPath, JSON.stringify({
19586
19624
  id: consensus_id,
19587
19625
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19588
19626
  agentCount: report.agentCount,
@@ -19622,10 +19660,10 @@ function persistPendingConsensus() {
19622
19660
  try {
19623
19661
  const projectRoot = ctx.mainAgent?.projectRoot;
19624
19662
  if (!projectRoot) return;
19625
- const { writeFileSync: writeFileSync17, mkdirSync: mkdirSync21 } = require("fs");
19626
- const { join: join48 } = require("path");
19627
- const dir = join48(projectRoot, ".gossip");
19628
- mkdirSync21(dir, { recursive: true });
19663
+ const { writeFileSync: writeFileSync18, mkdirSync: mkdirSync22 } = require("fs");
19664
+ const { join: join49 } = require("path");
19665
+ const dir = join49(projectRoot, ".gossip");
19666
+ mkdirSync22(dir, { recursive: true });
19629
19667
  const rounds = {};
19630
19668
  for (const [id, round] of ctx.pendingConsensusRounds) {
19631
19669
  rounds[id] = {
@@ -19647,7 +19685,7 @@ function persistPendingConsensus() {
19647
19685
  nativePrompts: (round.nativePrompts || []).filter((p) => round.pendingNativeAgents.has(p.agentId))
19648
19686
  };
19649
19687
  }
19650
- writeFileSync17(join48(dir, CONSENSUS_FILE), JSON.stringify(rounds));
19688
+ writeFileSync18(join49(dir, CONSENSUS_FILE), JSON.stringify(rounds));
19651
19689
  } catch (err) {
19652
19690
  process.stderr.write(`[gossipcat] persistPendingConsensus failed: ${err.message}
19653
19691
  `);
@@ -19655,11 +19693,11 @@ function persistPendingConsensus() {
19655
19693
  }
19656
19694
  function restorePendingConsensus(projectRoot) {
19657
19695
  try {
19658
- const { existsSync: existsSync43, readFileSync: readFileSync40, unlinkSync: unlinkSync4 } = require("fs");
19659
- const { join: join48 } = require("path");
19660
- const filePath = join48(projectRoot, ".gossip", CONSENSUS_FILE);
19661
- if (!existsSync43(filePath)) return;
19662
- const raw = JSON.parse(readFileSync40(filePath, "utf-8"));
19696
+ const { existsSync: existsSync44, readFileSync: readFileSync41, unlinkSync: unlinkSync4 } = require("fs");
19697
+ const { join: join49 } = require("path");
19698
+ const filePath = join49(projectRoot, ".gossip", CONSENSUS_FILE);
19699
+ if (!existsSync44(filePath)) return;
19700
+ const raw = JSON.parse(readFileSync41(filePath, "utf-8"));
19663
19701
  const now = Date.now();
19664
19702
  for (const [id, data] of Object.entries(raw)) {
19665
19703
  if (now > data.deadline + 3e5) {
@@ -20419,12 +20457,12 @@ async function overviewHandler(projectRoot, ctx2) {
20419
20457
  const hourlyActivity = new Array(12).fill(0);
20420
20458
  const now = Date.now();
20421
20459
  const hourMs = 60 * 60 * 1e3;
20422
- const graphPath = (0, import_path36.join)(projectRoot, ".gossip", "task-graph.jsonl");
20423
- if ((0, import_fs34.existsSync)(graphPath)) {
20460
+ const graphPath = (0, import_path37.join)(projectRoot, ".gossip", "task-graph.jsonl");
20461
+ if ((0, import_fs35.existsSync)(graphPath)) {
20424
20462
  try {
20425
20463
  const created = /* @__PURE__ */ new Map();
20426
20464
  const finished = /* @__PURE__ */ new Set();
20427
- const lines = (0, import_fs34.readFileSync)(graphPath, "utf-8").trim().split("\n").filter(Boolean);
20465
+ const lines = (0, import_fs35.readFileSync)(graphPath, "utf-8").trim().split("\n").filter(Boolean);
20428
20466
  for (const line of lines) {
20429
20467
  try {
20430
20468
  const ev = JSON.parse(line);
@@ -20474,17 +20512,27 @@ async function overviewHandler(projectRoot, ctx2) {
20474
20512
  let confirmedFindings = 0;
20475
20513
  let lastConsensusTimestamp = "";
20476
20514
  let actionableFindings = 0;
20477
- const consensusTaskIds = /* @__PURE__ */ new Set();
20478
- const perfPath = (0, import_path36.join)(projectRoot, ".gossip", "agent-performance.jsonl");
20479
- if ((0, import_fs34.existsSync)(perfPath)) {
20515
+ const runBuckets = /* @__PURE__ */ new Map();
20516
+ const perfPath = (0, import_path37.join)(projectRoot, ".gossip", "agent-performance.jsonl");
20517
+ if ((0, import_fs35.existsSync)(perfPath)) {
20480
20518
  try {
20481
- const lines = (0, import_fs34.readFileSync)(perfPath, "utf-8").trim().split("\n").filter(Boolean);
20519
+ const lines = (0, import_fs35.readFileSync)(perfPath, "utf-8").trim().split("\n").filter(Boolean);
20482
20520
  for (const line of lines) {
20483
20521
  try {
20484
20522
  const entry = JSON.parse(line);
20485
- totalSignals++;
20523
+ if (entry.type === "consensus" && typeof entry.signal === "string") {
20524
+ totalSignals++;
20525
+ }
20486
20526
  if (entry.type === "consensus" && (entry.consensusId || entry.taskId)) {
20487
- consensusTaskIds.add(entry.consensusId ?? entry.taskId);
20527
+ const runId = entry.consensusId ?? entry.taskId;
20528
+ let bucket = runBuckets.get(runId);
20529
+ if (!bucket) {
20530
+ bucket = { agents: /* @__PURE__ */ new Set(), signalCount: 0 };
20531
+ runBuckets.set(runId, bucket);
20532
+ }
20533
+ bucket.signalCount++;
20534
+ if (entry.agentId) bucket.agents.add(entry.agentId);
20535
+ if (entry.counterpartId) bucket.agents.add(entry.counterpartId);
20488
20536
  }
20489
20537
  if (entry.consensusId && entry.timestamp > lastConsensusTimestamp) {
20490
20538
  lastConsensusTimestamp = entry.timestamp;
@@ -20507,27 +20555,29 @@ async function overviewHandler(projectRoot, ctx2) {
20507
20555
  } catch {
20508
20556
  }
20509
20557
  }
20510
- consensusRuns = consensusTaskIds.size;
20558
+ for (const bucket of runBuckets.values()) {
20559
+ if (bucket.agents.size >= 2 && bucket.signalCount >= 3) consensusRuns++;
20560
+ }
20511
20561
  const avgDurationMs = durationCount > 0 ? Math.round(totalDuration / durationCount) : 0;
20512
20562
  return { agentsOnline, relayCount, relayConnected, nativeCount, consensusRuns, totalFindings, confirmedFindings, totalSignals, tasksCompleted, tasksFailed, avgDurationMs, lastConsensusTimestamp, actionableFindings, hourlyActivity };
20513
20563
  }
20514
- var import_fs34, import_path36;
20564
+ var import_fs35, import_path37;
20515
20565
  var init_api_overview = __esm({
20516
20566
  "packages/relay/src/dashboard/api-overview.ts"() {
20517
20567
  "use strict";
20518
- import_fs34 = require("fs");
20519
- import_path36 = require("path");
20568
+ import_fs35 = require("fs");
20569
+ import_path37 = require("path");
20520
20570
  }
20521
20571
  });
20522
20572
 
20523
20573
  // packages/relay/src/dashboard/api-agents.ts
20524
20574
  function readTaskGraphByAgent(projectRoot) {
20525
- const taskGraphPath = (0, import_path37.join)(projectRoot, ".gossip", "task-graph.jsonl");
20575
+ const taskGraphPath = (0, import_path38.join)(projectRoot, ".gossip", "task-graph.jsonl");
20526
20576
  const result = /* @__PURE__ */ new Map();
20527
- if (!(0, import_fs35.existsSync)(taskGraphPath)) return result;
20577
+ if (!(0, import_fs36.existsSync)(taskGraphPath)) return result;
20528
20578
  let lines;
20529
20579
  try {
20530
- lines = (0, import_fs35.readFileSync)(taskGraphPath, "utf-8").trim().split("\n").filter(Boolean);
20580
+ lines = (0, import_fs36.readFileSync)(taskGraphPath, "utf-8").trim().split("\n").filter(Boolean);
20531
20581
  } catch {
20532
20582
  return result;
20533
20583
  }
@@ -20619,19 +20669,32 @@ async function agentsHandler(projectRoot, configs, onlineAgents = []) {
20619
20669
  hallucinations: score.hallucinations,
20620
20670
  consecutiveFailures: score.consecutiveFailures ?? 0,
20621
20671
  circuitOpen: score.circuitOpen ?? false,
20622
- categoryStrengths: score.categoryStrengths ?? {}
20672
+ // categoryStrengths is an UNBOUNDED severity-weighted accumulator used
20673
+ // for dispatch routing (severity × decay × 0.15 per confirmed signal),
20674
+ // not a [0,1] ratio. The dashboard must NOT render it as a percentage —
20675
+ // it should render categoryAccuracy (c / (c + h)) which is a real ratio.
20676
+ // See performance-reader.ts:357 (increment) vs :496-505 (accuracy).
20677
+ categoryStrengths: score.categoryStrengths ?? {},
20678
+ categoryAccuracy: score.categoryAccuracy ?? {},
20679
+ // Forward raw counts so the dashboard can render "100% (3/3)" and
20680
+ // distinguish a sparse-but-clean category from a high-volume clean
20681
+ // one. categoryAccuracy already drops categories with fewer than
20682
+ // MIN_CATEGORY_N signals, but the counts let the UI surface them
20683
+ // as dimmed "sparse" rows instead of hiding them silently.
20684
+ categoryCorrect: score.categoryCorrect ?? {},
20685
+ categoryHallucinated: score.categoryHallucinated ?? {}
20623
20686
  }
20624
20687
  };
20625
20688
  });
20626
20689
  }
20627
- var import_fs35, import_path37, DEFAULT_SCORE;
20690
+ var import_fs36, import_path38, DEFAULT_SCORE;
20628
20691
  var init_api_agents = __esm({
20629
20692
  "packages/relay/src/dashboard/api-agents.ts"() {
20630
20693
  "use strict";
20631
20694
  init_performance_reader();
20632
20695
  init_skill_index();
20633
- import_fs35 = require("fs");
20634
- import_path37 = require("path");
20696
+ import_fs36 = require("fs");
20697
+ import_path38 = require("path");
20635
20698
  DEFAULT_SCORE = {
20636
20699
  agentId: "",
20637
20700
  accuracy: 0.5,
@@ -20655,7 +20718,7 @@ var init_api_agents = __esm({
20655
20718
 
20656
20719
  // packages/relay/src/dashboard/api-skills.ts
20657
20720
  function isCorrupt(projectRoot, index) {
20658
- return (0, import_fs36.existsSync)((0, import_path38.join)(projectRoot, ".gossip", "skill-index.json")) && !index.exists();
20721
+ return (0, import_fs37.existsSync)((0, import_path39.join)(projectRoot, ".gossip", "skill-index.json")) && !index.exists();
20659
20722
  }
20660
20723
  async function skillsGetHandler(projectRoot) {
20661
20724
  try {
@@ -20684,13 +20747,13 @@ async function skillsBindHandler(projectRoot, body) {
20684
20747
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
20685
20748
  }
20686
20749
  }
20687
- var import_fs36, import_path38, AGENT_ID_RE2;
20750
+ var import_fs37, import_path39, AGENT_ID_RE2;
20688
20751
  var init_api_skills = __esm({
20689
20752
  "packages/relay/src/dashboard/api-skills.ts"() {
20690
20753
  "use strict";
20691
20754
  init_skill_index();
20692
- import_fs36 = require("fs");
20693
- import_path38 = require("path");
20755
+ import_fs37 = require("fs");
20756
+ import_path39 = require("path");
20694
20757
  AGENT_ID_RE2 = /^[a-zA-Z0-9_-]{1,64}$/;
20695
20758
  }
20696
20759
  });
@@ -20698,25 +20761,25 @@ var init_api_skills = __esm({
20698
20761
  // packages/relay/src/dashboard/api-memory.ts
20699
20762
  async function memoryHandler(projectRoot, agentId) {
20700
20763
  if (!agentId || !AGENT_ID_RE3.test(agentId) || DANGEROUS_IDS.has(agentId)) throw new Error("Invalid agent ID");
20701
- const memDir = (0, import_path39.join)(projectRoot, ".gossip", "agents", agentId, "memory");
20764
+ const memDir = (0, import_path40.join)(projectRoot, ".gossip", "agents", agentId, "memory");
20702
20765
  let index = "";
20703
- const indexPath = (0, import_path39.join)(memDir, "MEMORY.md");
20704
- if ((0, import_fs37.existsSync)(indexPath)) {
20766
+ const indexPath = (0, import_path40.join)(memDir, "MEMORY.md");
20767
+ if ((0, import_fs38.existsSync)(indexPath)) {
20705
20768
  try {
20706
- index = (0, import_fs37.readFileSync)(indexPath, "utf-8");
20769
+ index = (0, import_fs38.readFileSync)(indexPath, "utf-8");
20707
20770
  } catch {
20708
20771
  }
20709
20772
  }
20710
20773
  const knowledge = [];
20711
- const knowledgeDir = (0, import_path39.join)(memDir, "knowledge");
20774
+ const knowledgeDir = (0, import_path40.join)(memDir, "knowledge");
20712
20775
  const knowledgeDirs = [knowledgeDir, memDir];
20713
20776
  for (const dir of knowledgeDirs) {
20714
- if (!(0, import_fs37.existsSync)(dir)) continue;
20777
+ if (!(0, import_fs38.existsSync)(dir)) continue;
20715
20778
  try {
20716
- const files = (0, import_fs37.readdirSync)(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
20779
+ const files = (0, import_fs38.readdirSync)(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
20717
20780
  for (const filename of files) {
20718
20781
  try {
20719
- const raw = (0, import_fs37.readFileSync)((0, import_path39.join)(dir, filename), "utf-8");
20782
+ const raw = (0, import_fs38.readFileSync)((0, import_path40.join)(dir, filename), "utf-8");
20720
20783
  const { frontmatter, content } = parseFrontmatter(raw);
20721
20784
  knowledge.push({ filename, frontmatter, content });
20722
20785
  } catch {
@@ -20726,10 +20789,10 @@ async function memoryHandler(projectRoot, agentId) {
20726
20789
  }
20727
20790
  }
20728
20791
  const tasks = [];
20729
- const tasksPath = (0, import_path39.join)(memDir, "tasks.jsonl");
20730
- if ((0, import_fs37.existsSync)(tasksPath)) {
20792
+ const tasksPath = (0, import_path40.join)(memDir, "tasks.jsonl");
20793
+ if ((0, import_fs38.existsSync)(tasksPath)) {
20731
20794
  try {
20732
- const lines = (0, import_fs37.readFileSync)(tasksPath, "utf-8").trim().split("\n").filter(Boolean).slice(-200);
20795
+ const lines = (0, import_fs38.readFileSync)(tasksPath, "utf-8").trim().split("\n").filter(Boolean).slice(-200);
20733
20796
  for (const line of lines) {
20734
20797
  try {
20735
20798
  tasks.push(JSON.parse(line));
@@ -20757,12 +20820,12 @@ function parseFrontmatter(raw) {
20757
20820
  }
20758
20821
  return { frontmatter: fm, content: raw.slice(end + 3).trim() };
20759
20822
  }
20760
- var import_fs37, import_path39, AGENT_ID_RE3, DANGEROUS_IDS;
20823
+ var import_fs38, import_path40, AGENT_ID_RE3, DANGEROUS_IDS;
20761
20824
  var init_api_memory = __esm({
20762
20825
  "packages/relay/src/dashboard/api-memory.ts"() {
20763
20826
  "use strict";
20764
- import_fs37 = require("fs");
20765
- import_path39 = require("path");
20827
+ import_fs38 = require("fs");
20828
+ import_path40 = require("path");
20766
20829
  AGENT_ID_RE3 = /^[a-zA-Z0-9_-]{1,64}$/;
20767
20830
  DANGEROUS_IDS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
20768
20831
  }
@@ -20774,11 +20837,11 @@ async function consensusHandler(projectRoot, query) {
20774
20837
  const rawPageSize = parseInt(query?.get("pageSize") ?? "", 10);
20775
20838
  const page = isNaN(rawPage) || rawPage < 1 ? 1 : rawPage;
20776
20839
  const pageSize = isNaN(rawPageSize) || rawPageSize < 1 ? DEFAULT_PAGE_SIZE : Math.min(rawPageSize, MAX_PAGE_SIZE);
20777
- const perfPath = (0, import_path40.join)(projectRoot, ".gossip", "agent-performance.jsonl");
20778
- if (!(0, import_fs38.existsSync)(perfPath)) return { runs: [], totalRuns: 0, totalSignals: 0, page, pageSize };
20840
+ const perfPath = (0, import_path41.join)(projectRoot, ".gossip", "agent-performance.jsonl");
20841
+ if (!(0, import_fs39.existsSync)(perfPath)) return { runs: [], totalRuns: 0, totalSignals: 0, page, pageSize };
20779
20842
  const signals = [];
20780
20843
  try {
20781
- const lines = (0, import_fs38.readFileSync)(perfPath, "utf-8").trim().split("\n").filter(Boolean);
20844
+ const lines = (0, import_fs39.readFileSync)(perfPath, "utf-8").trim().split("\n").filter(Boolean);
20782
20845
  for (const line of lines) {
20783
20846
  try {
20784
20847
  const parsed = JSON.parse(line);
@@ -20849,12 +20912,12 @@ async function consensusHandler(projectRoot, query) {
20849
20912
  const paginatedRuns = runs.slice(offset, offset + pageSize);
20850
20913
  return { runs: paginatedRuns, totalRuns, totalSignals: signals.length, page, pageSize };
20851
20914
  }
20852
- var import_fs38, import_path40, RESOLUTION_SIGNALS, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE;
20915
+ var import_fs39, import_path41, RESOLUTION_SIGNALS, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE;
20853
20916
  var init_api_consensus = __esm({
20854
20917
  "packages/relay/src/dashboard/api-consensus.ts"() {
20855
20918
  "use strict";
20856
- import_fs38 = require("fs");
20857
- import_path40 = require("path");
20919
+ import_fs39 = require("fs");
20920
+ import_path41 = require("path");
20858
20921
  RESOLUTION_SIGNALS = /* @__PURE__ */ new Set(["agreement", "unique_confirmed", "consensus_verified"]);
20859
20922
  DEFAULT_PAGE_SIZE = 10;
20860
20923
  MAX_PAGE_SIZE = 50;
@@ -20866,11 +20929,11 @@ async function signalsHandler(projectRoot, query) {
20866
20929
  const agentFilter = query?.get("agent") ?? null;
20867
20930
  const limit = Math.min(Math.max(parseInt(query?.get("limit") ?? "", 10) || DEFAULT_LIMIT, 1), MAX_LIMIT);
20868
20931
  const offset = Math.max(parseInt(query?.get("offset") ?? "", 10) || 0, 0);
20869
- const perfPath = (0, import_path41.join)(projectRoot, ".gossip", "agent-performance.jsonl");
20870
- if (!(0, import_fs39.existsSync)(perfPath)) return { items: [], total: 0, offset, limit };
20932
+ const perfPath = (0, import_path42.join)(projectRoot, ".gossip", "agent-performance.jsonl");
20933
+ if (!(0, import_fs40.existsSync)(perfPath)) return { items: [], total: 0, offset, limit };
20871
20934
  const all = [];
20872
20935
  try {
20873
- const lines = (0, import_fs39.readFileSync)(perfPath, "utf-8").trim().split("\n").filter(Boolean);
20936
+ const lines = (0, import_fs40.readFileSync)(perfPath, "utf-8").trim().split("\n").filter(Boolean);
20874
20937
  for (const line of lines) {
20875
20938
  try {
20876
20939
  const entry = JSON.parse(line);
@@ -20886,12 +20949,12 @@ async function signalsHandler(projectRoot, query) {
20886
20949
  all.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
20887
20950
  return { items: all.slice(offset, offset + limit), total: all.length, offset, limit };
20888
20951
  }
20889
- var import_fs39, import_path41, MAX_LIMIT, DEFAULT_LIMIT;
20952
+ var import_fs40, import_path42, MAX_LIMIT, DEFAULT_LIMIT;
20890
20953
  var init_api_signals = __esm({
20891
20954
  "packages/relay/src/dashboard/api-signals.ts"() {
20892
20955
  "use strict";
20893
- import_fs39 = require("fs");
20894
- import_path41 = require("path");
20956
+ import_fs40 = require("fs");
20957
+ import_path42 = require("path");
20895
20958
  MAX_LIMIT = 200;
20896
20959
  DEFAULT_LIMIT = 50;
20897
20960
  }
@@ -20899,29 +20962,29 @@ var init_api_signals = __esm({
20899
20962
 
20900
20963
  // packages/relay/src/dashboard/api-learnings.ts
20901
20964
  async function learningsHandler(projectRoot) {
20902
- const agentsDir = (0, import_path42.join)(projectRoot, ".gossip", "agents");
20903
- if (!(0, import_fs40.existsSync)(agentsDir)) return { learnings: [] };
20965
+ const agentsDir = (0, import_path43.join)(projectRoot, ".gossip", "agents");
20966
+ if (!(0, import_fs41.existsSync)(agentsDir)) return { learnings: [] };
20904
20967
  const all = [];
20905
20968
  let agentIds;
20906
20969
  try {
20907
- agentIds = (0, import_fs40.readdirSync)(agentsDir).filter((f) => !f.startsWith("."));
20970
+ agentIds = (0, import_fs41.readdirSync)(agentsDir).filter((f) => !f.startsWith("."));
20908
20971
  } catch {
20909
20972
  return { learnings: [] };
20910
20973
  }
20911
20974
  for (const agentId of agentIds) {
20912
- const knowledgeDir = (0, import_path42.join)(agentsDir, agentId, "memory", "knowledge");
20913
- if (!(0, import_fs40.existsSync)(knowledgeDir)) continue;
20975
+ const knowledgeDir = (0, import_path43.join)(agentsDir, agentId, "memory", "knowledge");
20976
+ if (!(0, import_fs41.existsSync)(knowledgeDir)) continue;
20914
20977
  let files;
20915
20978
  try {
20916
- files = (0, import_fs40.readdirSync)(knowledgeDir).filter((f) => f.endsWith(".md"));
20979
+ files = (0, import_fs41.readdirSync)(knowledgeDir).filter((f) => f.endsWith(".md"));
20917
20980
  } catch {
20918
20981
  continue;
20919
20982
  }
20920
20983
  for (const filename of files) {
20921
- const filepath = (0, import_path42.join)(knowledgeDir, filename);
20984
+ const filepath = (0, import_path43.join)(knowledgeDir, filename);
20922
20985
  try {
20923
- const stat3 = (0, import_fs40.statSync)(filepath);
20924
- const raw = (0, import_fs40.readFileSync)(filepath, "utf-8");
20986
+ const stat3 = (0, import_fs41.statSync)(filepath);
20987
+ const raw = (0, import_fs41.readFileSync)(filepath, "utf-8");
20925
20988
  const fm = parseFrontmatter2(raw);
20926
20989
  all.push({
20927
20990
  agentId,
@@ -20948,12 +21011,12 @@ function parseFrontmatter2(raw) {
20948
21011
  }
20949
21012
  return fm;
20950
21013
  }
20951
- var import_fs40, import_path42, MAX_LEARNINGS;
21014
+ var import_fs41, import_path43, MAX_LEARNINGS;
20952
21015
  var init_api_learnings = __esm({
20953
21016
  "packages/relay/src/dashboard/api-learnings.ts"() {
20954
21017
  "use strict";
20955
- import_fs40 = require("fs");
20956
- import_path42 = require("path");
21018
+ import_fs41 = require("fs");
21019
+ import_path43 = require("path");
20957
21020
  MAX_LEARNINGS = 10;
20958
21021
  }
20959
21022
  });
@@ -20964,12 +21027,12 @@ async function tasksHandler(projectRoot, query) {
20964
21027
  const rawOffset = parseInt(query?.get("offset") ?? "0", 10);
20965
21028
  const limit = isNaN(rawLimit) || rawLimit < 1 ? 50 : Math.min(rawLimit, 200);
20966
21029
  const offset = isNaN(rawOffset) || rawOffset < 0 ? 0 : rawOffset;
20967
- const graphPath = (0, import_path43.join)(projectRoot, ".gossip", "task-graph.jsonl");
20968
- if (!(0, import_fs41.existsSync)(graphPath)) return { items: [], total: 0, offset, limit };
21030
+ const graphPath = (0, import_path44.join)(projectRoot, ".gossip", "task-graph.jsonl");
21031
+ if (!(0, import_fs42.existsSync)(graphPath)) return { items: [], total: 0, offset, limit };
20969
21032
  const created = /* @__PURE__ */ new Map();
20970
21033
  const completed = /* @__PURE__ */ new Map();
20971
21034
  try {
20972
- const lines = (0, import_fs41.readFileSync)(graphPath, "utf-8").trim().split("\n").filter(Boolean);
21035
+ const lines = (0, import_fs42.readFileSync)(graphPath, "utf-8").trim().split("\n").filter(Boolean);
20973
21036
  for (const line of lines) {
20974
21037
  try {
20975
21038
  const entry = JSON.parse(line);
@@ -21024,23 +21087,23 @@ async function tasksHandler(projectRoot, query) {
21024
21087
  tasks.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
21025
21088
  return { items: tasks.slice(offset, offset + limit), total: tasks.length, offset, limit };
21026
21089
  }
21027
- var import_fs41, import_path43;
21090
+ var import_fs42, import_path44;
21028
21091
  var init_api_tasks = __esm({
21029
21092
  "packages/relay/src/dashboard/api-tasks.ts"() {
21030
21093
  "use strict";
21031
- import_fs41 = require("fs");
21032
- import_path43 = require("path");
21094
+ import_fs42 = require("fs");
21095
+ import_path44 = require("path");
21033
21096
  }
21034
21097
  });
21035
21098
 
21036
21099
  // packages/relay/src/dashboard/api-active-tasks.ts
21037
21100
  async function activeTasksHandler(projectRoot) {
21038
- const taskGraphPath = (0, import_path44.join)(projectRoot, ".gossip", "task-graph.jsonl");
21039
- if (!(0, import_fs42.existsSync)(taskGraphPath)) return { tasks: [] };
21101
+ const taskGraphPath = (0, import_path45.join)(projectRoot, ".gossip", "task-graph.jsonl");
21102
+ if (!(0, import_fs43.existsSync)(taskGraphPath)) return { tasks: [] };
21040
21103
  const created = /* @__PURE__ */ new Map();
21041
21104
  const finished = /* @__PURE__ */ new Set();
21042
21105
  try {
21043
- const lines = (0, import_fs42.readFileSync)(taskGraphPath, "utf-8").trim().split("\n").filter(Boolean);
21106
+ const lines = (0, import_fs43.readFileSync)(taskGraphPath, "utf-8").trim().split("\n").filter(Boolean);
21044
21107
  for (const line of lines) {
21045
21108
  try {
21046
21109
  const ev = JSON.parse(line);
@@ -21067,12 +21130,12 @@ async function activeTasksHandler(projectRoot) {
21067
21130
  active.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
21068
21131
  return { tasks: active.slice(0, 10) };
21069
21132
  }
21070
- var import_fs42, import_path44;
21133
+ var import_fs43, import_path45;
21071
21134
  var init_api_active_tasks = __esm({
21072
21135
  "packages/relay/src/dashboard/api-active-tasks.ts"() {
21073
21136
  "use strict";
21074
- import_fs42 = require("fs");
21075
- import_path44 = require("path");
21137
+ import_fs43 = require("fs");
21138
+ import_path45 = require("path");
21076
21139
  }
21077
21140
  });
21078
21141
 
@@ -21085,25 +21148,25 @@ function categorize(text) {
21085
21148
  return "other";
21086
21149
  }
21087
21150
  function logsHandler(projectRoot, query) {
21088
- const logPath = (0, import_path45.join)(projectRoot, ".gossip", "mcp.log");
21089
- if (!(0, import_fs43.existsSync)(logPath)) {
21151
+ const logPath = (0, import_path46.join)(projectRoot, ".gossip", "mcp.log");
21152
+ if (!(0, import_fs44.existsSync)(logPath)) {
21090
21153
  return { entries: [], totalLines: 0, fileSize: 0 };
21091
21154
  }
21092
21155
  const filter = query?.get("filter") || void 0;
21093
21156
  const tail = parseInt(query?.get("tail") || "200", 10);
21094
21157
  const clampedTail = Math.min(Math.max(tail, 10), 2e3);
21095
- const fileSize = (0, import_fs43.statSync)(logPath).size;
21158
+ const fileSize = (0, import_fs44.statSync)(logPath).size;
21096
21159
  const MAX_READ = 512 * 1024;
21097
21160
  const readFrom = Math.max(0, fileSize - MAX_READ);
21098
21161
  const readLen = fileSize - readFrom;
21099
- const fd = (0, import_fs43.openSync)(logPath, "r");
21162
+ const fd = (0, import_fs44.openSync)(logPath, "r");
21100
21163
  let buf = Buffer.alloc(0);
21101
21164
  try {
21102
21165
  buf = Buffer.allocUnsafe(readLen);
21103
- const bytesRead = (0, import_fs43.readSync)(fd, buf, 0, readLen, readFrom);
21166
+ const bytesRead = (0, import_fs44.readSync)(fd, buf, 0, readLen, readFrom);
21104
21167
  buf = buf.subarray(0, bytesRead);
21105
21168
  } finally {
21106
- (0, import_fs43.closeSync)(fd);
21169
+ (0, import_fs44.closeSync)(fd);
21107
21170
  }
21108
21171
  let raw = buf.toString("utf-8");
21109
21172
  let lineOffset = 0;
@@ -21111,13 +21174,13 @@ function logsHandler(projectRoot, query) {
21111
21174
  const nl = raw.indexOf("\n");
21112
21175
  raw = nl >= 0 ? raw.slice(nl + 1) : raw;
21113
21176
  const SCAN_CHUNK = 64 * 1024;
21114
- const scanFd = (0, import_fs43.openSync)(logPath, "r");
21177
+ const scanFd = (0, import_fs44.openSync)(logPath, "r");
21115
21178
  try {
21116
21179
  let pos = 0;
21117
21180
  const chunk = Buffer.allocUnsafe(SCAN_CHUNK);
21118
21181
  while (pos < readFrom) {
21119
21182
  const len = Math.min(SCAN_CHUNK, readFrom - pos);
21120
- const n = (0, import_fs43.readSync)(scanFd, chunk, 0, len, pos);
21183
+ const n = (0, import_fs44.readSync)(scanFd, chunk, 0, len, pos);
21121
21184
  if (n === 0) break;
21122
21185
  for (let j = 0; j < n; j++) {
21123
21186
  if (chunk[j] === 10) lineOffset++;
@@ -21125,7 +21188,7 @@ function logsHandler(projectRoot, query) {
21125
21188
  pos += n;
21126
21189
  }
21127
21190
  } finally {
21128
- (0, import_fs43.closeSync)(scanFd);
21191
+ (0, import_fs44.closeSync)(scanFd);
21129
21192
  }
21130
21193
  }
21131
21194
  const allLines = raw.split("\n").filter(Boolean);
@@ -21143,12 +21206,12 @@ function logsHandler(projectRoot, query) {
21143
21206
  entries = entries.slice(-clampedTail);
21144
21207
  return { entries, totalLines, fileSize, filter };
21145
21208
  }
21146
- var import_fs43, import_path45, CATEGORY_PATTERNS2;
21209
+ var import_fs44, import_path46, CATEGORY_PATTERNS2;
21147
21210
  var init_api_logs = __esm({
21148
21211
  "packages/relay/src/dashboard/api-logs.ts"() {
21149
21212
  "use strict";
21150
- import_fs43 = require("fs");
21151
- import_path45 = require("path");
21213
+ import_fs44 = require("fs");
21214
+ import_path46 = require("path");
21152
21215
  CATEGORY_PATTERNS2 = [
21153
21216
  [/^\[worker:/, "worker"],
21154
21217
  [/^\[Gemini\]/, "gemini"],
@@ -21178,15 +21241,15 @@ var init_api_logs = __esm({
21178
21241
  // packages/relay/src/dashboard/routes.ts
21179
21242
  function resolveDashboardRoot(projectRoot) {
21180
21243
  const candidates = [
21181
- (0, import_path46.resolve)(__dirname, "..", "dist-dashboard"),
21244
+ (0, import_path47.resolve)(__dirname, "..", "dist-dashboard"),
21182
21245
  // bundled: dist-mcp/mcp-server.js → ../dist-dashboard
21183
- (0, import_path46.resolve)(__dirname, "..", "..", "..", "..", "dist-dashboard"),
21246
+ (0, import_path47.resolve)(__dirname, "..", "..", "..", "..", "dist-dashboard"),
21184
21247
  // tsc dev: packages/relay/dist/dashboard → repo-root
21185
- (0, import_path46.join)(projectRoot, "dist-dashboard")
21248
+ (0, import_path47.join)(projectRoot, "dist-dashboard")
21186
21249
  // legacy dev fallback (git-clone running from repo root)
21187
21250
  ];
21188
21251
  for (const p of candidates) {
21189
- if ((0, import_fs44.existsSync)(p)) return p;
21252
+ if ((0, import_fs45.existsSync)(p)) return p;
21190
21253
  }
21191
21254
  return null;
21192
21255
  }
@@ -21213,7 +21276,7 @@ function readBody(req) {
21213
21276
  });
21214
21277
  });
21215
21278
  }
21216
- var import_fs44, import_path46, AUTH_MAX_ATTEMPTS, AUTH_LOCKOUT_MS, DashboardRouter, MAX_BODY_SIZE;
21279
+ var import_fs45, import_path47, AUTH_MAX_ATTEMPTS, AUTH_LOCKOUT_MS, DashboardRouter, MAX_BODY_SIZE;
21217
21280
  var init_routes = __esm({
21218
21281
  "packages/relay/src/dashboard/routes.ts"() {
21219
21282
  "use strict";
@@ -21227,8 +21290,8 @@ var init_routes = __esm({
21227
21290
  init_api_tasks();
21228
21291
  init_api_active_tasks();
21229
21292
  init_api_logs();
21230
- import_fs44 = require("fs");
21231
- import_path46 = require("path");
21293
+ import_fs45 = require("fs");
21294
+ import_path47 = require("path");
21232
21295
  AUTH_MAX_ATTEMPTS = 10;
21233
21296
  AUTH_LOCKOUT_MS = 6e4;
21234
21297
  DashboardRouter = class {
@@ -21410,13 +21473,13 @@ var init_routes = __esm({
21410
21473
  res.end("Dashboard assets not found. Reinstall gossipcat or rebuild from source.");
21411
21474
  return true;
21412
21475
  }
21413
- const htmlPath = (0, import_path46.join)(this.dashboardRoot, "index.html");
21414
- if (!(0, import_fs44.existsSync)(htmlPath)) {
21476
+ const htmlPath = (0, import_path47.join)(this.dashboardRoot, "index.html");
21477
+ if (!(0, import_fs45.existsSync)(htmlPath)) {
21415
21478
  res.writeHead(503, { "Content-Type": "text/plain" });
21416
21479
  res.end(`Dashboard index.html missing at ${this.dashboardRoot}. Reinstall gossipcat.`);
21417
21480
  return true;
21418
21481
  }
21419
- const html = (0, import_fs44.readFileSync)(htmlPath, "utf-8");
21482
+ const html = (0, import_fs45.readFileSync)(htmlPath, "utf-8");
21420
21483
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
21421
21484
  res.end(html);
21422
21485
  return true;
@@ -21442,16 +21505,16 @@ var init_routes = __esm({
21442
21505
  const ext = "." + (relativePath.split(".").pop() || "");
21443
21506
  const mime = MIME[ext];
21444
21507
  if (!mime) return false;
21445
- const filePath = (0, import_path46.join)(this.dashboardRoot, relativePath);
21508
+ const filePath = (0, import_path47.join)(this.dashboardRoot, relativePath);
21446
21509
  try {
21447
- const realFile = (0, import_fs44.realpathSync)(filePath);
21448
- const realBase = (0, import_fs44.realpathSync)(this.dashboardRoot);
21510
+ const realFile = (0, import_fs45.realpathSync)(filePath);
21511
+ const realBase = (0, import_fs45.realpathSync)(this.dashboardRoot);
21449
21512
  if (!realFile.startsWith(realBase + "/")) {
21450
21513
  res.writeHead(404);
21451
21514
  res.end();
21452
21515
  return true;
21453
21516
  }
21454
- const data = (0, import_fs44.readFileSync)(realFile);
21517
+ const data = (0, import_fs45.readFileSync)(realFile);
21455
21518
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=86400" });
21456
21519
  res.end(data);
21457
21520
  return true;
@@ -21466,15 +21529,15 @@ var init_routes = __esm({
21466
21529
  return match ? match[1] : null;
21467
21530
  }
21468
21531
  getConsensusReports(page = 1, pageSize = 5) {
21469
- const { readdirSync: readdirSync13, readFileSync: readFileSync40, existsSync: existsSync43 } = require("fs");
21470
- const reportsDir = (0, import_path46.join)(this.projectRoot, ".gossip", "consensus-reports");
21471
- if (!existsSync43(reportsDir)) return { reports: [], totalReports: 0, page, pageSize };
21532
+ const { readdirSync: readdirSync13, readFileSync: readFileSync41, existsSync: existsSync44 } = require("fs");
21533
+ const reportsDir = (0, import_path47.join)(this.projectRoot, ".gossip", "consensus-reports");
21534
+ if (!existsSync44(reportsDir)) return { reports: [], totalReports: 0, page, pageSize };
21472
21535
  try {
21473
21536
  const { statSync: statSync9 } = require("fs");
21474
21537
  const allFiles = readdirSync13(reportsDir).filter((f) => f.endsWith(".json")).sort((a, b) => {
21475
21538
  try {
21476
- const aTime = statSync9((0, import_path46.join)(reportsDir, a)).mtimeMs;
21477
- const bTime = statSync9((0, import_path46.join)(reportsDir, b)).mtimeMs;
21539
+ const aTime = statSync9((0, import_path47.join)(reportsDir, a)).mtimeMs;
21540
+ const bTime = statSync9((0, import_path47.join)(reportsDir, b)).mtimeMs;
21478
21541
  return bTime - aTime;
21479
21542
  } catch {
21480
21543
  return 0;
@@ -21485,13 +21548,13 @@ var init_routes = __esm({
21485
21548
  const clampedPage = Math.max(page, 1);
21486
21549
  const start = (clampedPage - 1) * clampedPageSize;
21487
21550
  const files = allFiles.slice(start, start + clampedPageSize);
21488
- const realReportsDir = (0, import_fs44.realpathSync)(reportsDir);
21551
+ const realReportsDir = (0, import_fs45.realpathSync)(reportsDir);
21489
21552
  const reports = files.map((f) => {
21490
21553
  try {
21491
- const filePath = (0, import_path46.join)(reportsDir, f);
21492
- const realFile = (0, import_fs44.realpathSync)(filePath);
21554
+ const filePath = (0, import_path47.join)(reportsDir, f);
21555
+ const realFile = (0, import_fs45.realpathSync)(filePath);
21493
21556
  if (!realFile.startsWith(realReportsDir + "/")) return null;
21494
- return JSON.parse(readFileSync40(realFile, "utf-8"));
21557
+ return JSON.parse(readFileSync41(realFile, "utf-8"));
21495
21558
  } catch {
21496
21559
  return null;
21497
21560
  }
@@ -21502,29 +21565,29 @@ var init_routes = __esm({
21502
21565
  }
21503
21566
  }
21504
21567
  archiveFindings() {
21505
- const { readdirSync: readdirSync13, readFileSync: readFileSync40, renameSync: renameSync2, writeFileSync: writeFileSync17, mkdirSync: mkdirSync21, existsSync: existsSync43 } = require("fs");
21506
- const reportsDir = (0, import_path46.join)(this.projectRoot, ".gossip", "consensus-reports");
21507
- const archiveDir = (0, import_path46.join)(this.projectRoot, ".gossip", "consensus-reports-archive");
21568
+ const { readdirSync: readdirSync13, readFileSync: readFileSync41, renameSync: renameSync2, writeFileSync: writeFileSync18, mkdirSync: mkdirSync22, existsSync: existsSync44 } = require("fs");
21569
+ const reportsDir = (0, import_path47.join)(this.projectRoot, ".gossip", "consensus-reports");
21570
+ const archiveDir = (0, import_path47.join)(this.projectRoot, ".gossip", "consensus-reports-archive");
21508
21571
  let archived = 0;
21509
- if (existsSync43(reportsDir)) {
21572
+ if (existsSync44(reportsDir)) {
21510
21573
  const files = readdirSync13(reportsDir).filter((f) => f.endsWith(".json")).sort().reverse();
21511
21574
  if (files.length > 5) {
21512
- mkdirSync21(archiveDir, { recursive: true });
21575
+ mkdirSync22(archiveDir, { recursive: true });
21513
21576
  const toArchive = files.slice(5);
21514
21577
  for (const f of toArchive) {
21515
21578
  try {
21516
- renameSync2((0, import_path46.join)(reportsDir, f), (0, import_path46.join)(archiveDir, f));
21579
+ renameSync2((0, import_path47.join)(reportsDir, f), (0, import_path47.join)(archiveDir, f));
21517
21580
  archived++;
21518
21581
  } catch {
21519
21582
  }
21520
21583
  }
21521
21584
  }
21522
21585
  }
21523
- const findingsPath = (0, import_path46.join)(this.projectRoot, ".gossip", "implementation-findings.jsonl");
21586
+ const findingsPath = (0, import_path47.join)(this.projectRoot, ".gossip", "implementation-findings.jsonl");
21524
21587
  let findingsCleared = 0;
21525
- if (existsSync43(findingsPath)) {
21588
+ if (existsSync44(findingsPath)) {
21526
21589
  try {
21527
- const lines = readFileSync40(findingsPath, "utf-8").trim().split("\n").filter(Boolean);
21590
+ const lines = readFileSync41(findingsPath, "utf-8").trim().split("\n").filter(Boolean);
21528
21591
  const kept = lines.filter((line) => {
21529
21592
  try {
21530
21593
  const entry = JSON.parse(line);
@@ -21537,11 +21600,11 @@ var init_routes = __esm({
21537
21600
  return true;
21538
21601
  }
21539
21602
  });
21540
- writeFileSync17(findingsPath, kept.join("\n") + (kept.length > 0 ? "\n" : ""));
21603
+ writeFileSync18(findingsPath, kept.join("\n") + (kept.length > 0 ? "\n" : ""));
21541
21604
  } catch {
21542
21605
  }
21543
21606
  }
21544
- const remaining = existsSync43(reportsDir) ? readdirSync13(reportsDir).filter((f) => f.endsWith(".json")).length : 0;
21607
+ const remaining = existsSync44(reportsDir) ? readdirSync13(reportsDir).filter((f) => f.endsWith(".json")).length : 0;
21545
21608
  return { archived, remaining, findingsCleared };
21546
21609
  }
21547
21610
  json(res, status, data) {
@@ -21554,14 +21617,14 @@ var init_routes = __esm({
21554
21617
  });
21555
21618
 
21556
21619
  // packages/relay/src/dashboard/ws.ts
21557
- var import_ws3, import_fs45, import_fs46, import_path47, DashboardWs;
21620
+ var import_ws3, import_fs46, import_fs47, import_path48, DashboardWs;
21558
21621
  var init_ws = __esm({
21559
21622
  "packages/relay/src/dashboard/ws.ts"() {
21560
21623
  "use strict";
21561
21624
  import_ws3 = require("ws");
21562
- import_fs45 = require("fs");
21563
21625
  import_fs46 = require("fs");
21564
- import_path47 = require("path");
21626
+ import_fs47 = require("fs");
21627
+ import_path48 = require("path");
21565
21628
  DashboardWs = class {
21566
21629
  clients = /* @__PURE__ */ new Set();
21567
21630
  logWatcher = null;
@@ -21586,15 +21649,15 @@ var init_ws = __esm({
21586
21649
  /** Start watching mcp.log for new lines and broadcasting them to connected clients. */
21587
21650
  startLogWatcher(projectRoot) {
21588
21651
  this.stopLogWatcher();
21589
- this.logPath = (0, import_path47.join)(projectRoot, ".gossip", "mcp.log");
21590
- if (!(0, import_fs45.existsSync)(this.logPath)) return;
21652
+ this.logPath = (0, import_path48.join)(projectRoot, ".gossip", "mcp.log");
21653
+ if (!(0, import_fs46.existsSync)(this.logPath)) return;
21591
21654
  try {
21592
- this.logOffset = (0, import_fs45.statSync)(this.logPath).size;
21655
+ this.logOffset = (0, import_fs46.statSync)(this.logPath).size;
21593
21656
  } catch {
21594
21657
  this.logOffset = 0;
21595
21658
  }
21596
21659
  try {
21597
- this.logWatcher = (0, import_fs46.watch)(this.logPath, () => {
21660
+ this.logWatcher = (0, import_fs47.watch)(this.logPath, () => {
21598
21661
  if (this.clients.size === 0) return;
21599
21662
  this.readNewLines();
21600
21663
  });
@@ -21611,7 +21674,7 @@ var init_ws = __esm({
21611
21674
  if (this.logReading) return;
21612
21675
  this.logReading = true;
21613
21676
  try {
21614
- const currentSize = (0, import_fs45.statSync)(this.logPath).size;
21677
+ const currentSize = (0, import_fs46.statSync)(this.logPath).size;
21615
21678
  if (currentSize < this.logOffset) {
21616
21679
  this.logOffset = 0;
21617
21680
  this.logCarry = "";
@@ -21624,7 +21687,7 @@ var init_ws = __esm({
21624
21687
  const readFrom = capped ? (this.logCarry = "", this.logCapped = true, currentSize - 65536) : (this.logCapped = false, this.logOffset);
21625
21688
  let stream;
21626
21689
  try {
21627
- stream = (0, import_fs45.createReadStream)(this.logPath, { start: readFrom, end: currentSize - 1 });
21690
+ stream = (0, import_fs46.createReadStream)(this.logPath, { start: readFrom, end: currentSize - 1 });
21628
21691
  } catch {
21629
21692
  this.logReading = false;
21630
21693
  return;
@@ -21980,18 +22043,18 @@ __export(config_exports, {
21980
22043
  function findConfigPath(projectRoot) {
21981
22044
  const root = projectRoot || process.cwd();
21982
22045
  const candidates = [
21983
- (0, import_path48.resolve)(root, ".gossip", "config.json"),
21984
- (0, import_path48.resolve)(root, "gossip.agents.json"),
21985
- (0, import_path48.resolve)(root, "gossip.agents.yaml"),
21986
- (0, import_path48.resolve)(root, "gossip.agents.yml")
22046
+ (0, import_path49.resolve)(root, ".gossip", "config.json"),
22047
+ (0, import_path49.resolve)(root, "gossip.agents.json"),
22048
+ (0, import_path49.resolve)(root, "gossip.agents.yaml"),
22049
+ (0, import_path49.resolve)(root, "gossip.agents.yml")
21987
22050
  ];
21988
22051
  for (const p of candidates) {
21989
- if ((0, import_fs47.existsSync)(p)) return p;
22052
+ if ((0, import_fs48.existsSync)(p)) return p;
21990
22053
  }
21991
22054
  return null;
21992
22055
  }
21993
22056
  function loadConfig(configPath) {
21994
- const raw = (0, import_fs47.readFileSync)(configPath, "utf-8");
22057
+ const raw = (0, import_fs48.readFileSync)(configPath, "utf-8");
21995
22058
  let parsed;
21996
22059
  try {
21997
22060
  parsed = JSON.parse(raw);
@@ -22070,19 +22133,19 @@ function configToAgentConfigs(config2) {
22070
22133
  }
22071
22134
  function loadClaudeSubagents(projectRoot, existingIds) {
22072
22135
  const root = projectRoot || process.cwd();
22073
- const agentsDir = (0, import_path48.join)(root, ".claude", "agents");
22074
- if (!(0, import_fs47.existsSync)(agentsDir)) return [];
22136
+ const agentsDir = (0, import_path49.join)(root, ".claude", "agents");
22137
+ if (!(0, import_fs48.existsSync)(agentsDir)) return [];
22075
22138
  let files;
22076
22139
  try {
22077
- files = (0, import_fs47.readdirSync)(agentsDir).filter((f) => f.endsWith(".md"));
22140
+ files = (0, import_fs48.readdirSync)(agentsDir).filter((f) => f.endsWith(".md"));
22078
22141
  } catch {
22079
22142
  return [];
22080
22143
  }
22081
22144
  const agents = [];
22082
22145
  for (const file2 of files) {
22083
- const filePath = (0, import_path48.join)(agentsDir, file2);
22146
+ const filePath = (0, import_path49.join)(agentsDir, file2);
22084
22147
  try {
22085
- const content = (0, import_fs47.readFileSync)(filePath, "utf-8");
22148
+ const content = (0, import_fs48.readFileSync)(filePath, "utf-8");
22086
22149
  const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
22087
22150
  if (!frontmatter) continue;
22088
22151
  const fm = frontmatter[1];
@@ -22137,12 +22200,12 @@ function inferSkills(description, name) {
22137
22200
  if (skills.length === 0) skills.push("general");
22138
22201
  return skills;
22139
22202
  }
22140
- var import_fs47, import_path48, VALID_PROVIDERS, CLAUDE_MODEL_MAP;
22203
+ var import_fs48, import_path49, VALID_PROVIDERS, CLAUDE_MODEL_MAP;
22141
22204
  var init_config = __esm({
22142
22205
  "apps/cli/src/config.ts"() {
22143
22206
  "use strict";
22144
- import_fs47 = require("fs");
22145
- import_path48 = require("path");
22207
+ import_fs48 = require("fs");
22208
+ import_path49 = require("path");
22146
22209
  VALID_PROVIDERS = ["anthropic", "openai", "openclaw", "google", "local", "native"];
22147
22210
  CLAUDE_MODEL_MAP = {
22148
22211
  opus: { provider: "anthropic", model: "claude-opus-4-6" },
@@ -22157,14 +22220,14 @@ var keychain_exports = {};
22157
22220
  __export(keychain_exports, {
22158
22221
  Keychain: () => Keychain
22159
22222
  });
22160
- var import_child_process5, import_os2, import_fs48, import_path49, import_crypto15, SERVICE_NAME, VALID_PROVIDERS2, ENCRYPTED_FILE, ALGO, Keychain;
22223
+ var import_child_process5, import_os2, import_fs49, import_path50, import_crypto15, SERVICE_NAME, VALID_PROVIDERS2, ENCRYPTED_FILE, ALGO, Keychain;
22161
22224
  var init_keychain = __esm({
22162
22225
  "apps/cli/src/keychain.ts"() {
22163
22226
  "use strict";
22164
22227
  import_child_process5 = require("child_process");
22165
22228
  import_os2 = require("os");
22166
- import_fs48 = require("fs");
22167
- import_path49 = require("path");
22229
+ import_fs49 = require("fs");
22230
+ import_path50 = require("path");
22168
22231
  import_crypto15 = require("crypto");
22169
22232
  SERVICE_NAME = "gossip-mesh";
22170
22233
  VALID_PROVIDERS2 = /^[a-zA-Z0-9_-]{1,32}$/;
@@ -22206,10 +22269,10 @@ var init_keychain = __esm({
22206
22269
  return (0, import_crypto15.pbkdf2Sync)(seed, salt, 6e5, 32, "sha256");
22207
22270
  }
22208
22271
  loadEncryptedFile() {
22209
- const filePath = (0, import_path49.join)(process.cwd(), ENCRYPTED_FILE);
22210
- if (!(0, import_fs48.existsSync)(filePath)) return;
22272
+ const filePath = (0, import_path50.join)(process.cwd(), ENCRYPTED_FILE);
22273
+ if (!(0, import_fs49.existsSync)(filePath)) return;
22211
22274
  try {
22212
- const raw = (0, import_fs48.readFileSync)(filePath);
22275
+ const raw = (0, import_fs49.readFileSync)(filePath);
22213
22276
  if (raw.length < 61) return;
22214
22277
  const salt = raw.subarray(0, 32);
22215
22278
  const iv = raw.subarray(32, 44);
@@ -22227,9 +22290,9 @@ var init_keychain = __esm({
22227
22290
  }
22228
22291
  }
22229
22292
  saveEncryptedFile() {
22230
- const filePath = (0, import_path49.join)(process.cwd(), ENCRYPTED_FILE);
22231
- const dir = (0, import_path49.join)(process.cwd(), ".gossip");
22232
- if (!(0, import_fs48.existsSync)(dir)) (0, import_fs48.mkdirSync)(dir, { recursive: true });
22293
+ const filePath = (0, import_path50.join)(process.cwd(), ENCRYPTED_FILE);
22294
+ const dir = (0, import_path50.join)(process.cwd(), ".gossip");
22295
+ if (!(0, import_fs49.existsSync)(dir)) (0, import_fs49.mkdirSync)(dir, { recursive: true });
22233
22296
  const data = JSON.stringify(Object.fromEntries(this.inMemoryStore));
22234
22297
  const salt = (0, import_crypto15.randomBytes)(32);
22235
22298
  const iv = (0, import_crypto15.randomBytes)(12);
@@ -22237,7 +22300,7 @@ var init_keychain = __esm({
22237
22300
  const cipher = (0, import_crypto15.createCipheriv)(ALGO, key, iv);
22238
22301
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
22239
22302
  const tag = cipher.getAuthTag();
22240
- (0, import_fs48.writeFileSync)(filePath, Buffer.concat([salt, iv, tag, encrypted]), { mode: 384 });
22303
+ (0, import_fs49.writeFileSync)(filePath, Buffer.concat([salt, iv, tag, encrypted]), { mode: 384 });
22241
22304
  }
22242
22305
  isKeychainAvailable() {
22243
22306
  if ((0, import_os2.platform)() === "darwin") {
@@ -22337,17 +22400,17 @@ __export(identity_exports, {
22337
22400
  normalizeGitUrl: () => normalizeGitUrl
22338
22401
  });
22339
22402
  function getOrCreateSalt(projectRoot) {
22340
- const saltPath = (0, import_path50.join)(projectRoot, ".gossip", "local-salt");
22403
+ const saltPath = (0, import_path51.join)(projectRoot, ".gossip", "local-salt");
22341
22404
  try {
22342
- return (0, import_fs49.readFileSync)(saltPath, "utf-8").trim();
22405
+ return (0, import_fs50.readFileSync)(saltPath, "utf-8").trim();
22343
22406
  } catch {
22344
22407
  const salt = (0, import_crypto16.randomBytes)(16).toString("hex");
22345
- (0, import_fs49.mkdirSync)((0, import_path50.join)(projectRoot, ".gossip"), { recursive: true });
22408
+ (0, import_fs50.mkdirSync)((0, import_path51.join)(projectRoot, ".gossip"), { recursive: true });
22346
22409
  try {
22347
- (0, import_fs49.writeFileSync)(saltPath, salt, { flag: "wx" });
22410
+ (0, import_fs50.writeFileSync)(saltPath, salt, { flag: "wx" });
22348
22411
  return salt;
22349
22412
  } catch {
22350
- return (0, import_fs49.readFileSync)(saltPath, "utf-8").trim();
22413
+ return (0, import_fs50.readFileSync)(saltPath, "utf-8").trim();
22351
22414
  }
22352
22415
  }
22353
22416
  }
@@ -22397,12 +22460,12 @@ function getProjectId(projectRoot) {
22397
22460
  }
22398
22461
  return (0, import_crypto16.createHash)("sha256").update(projectRoot).digest("hex").slice(0, 16);
22399
22462
  }
22400
- var import_fs49, import_path50, import_crypto16, import_child_process6;
22463
+ var import_fs50, import_path51, import_crypto16, import_child_process6;
22401
22464
  var init_identity = __esm({
22402
22465
  "apps/cli/src/identity.ts"() {
22403
22466
  "use strict";
22404
- import_fs49 = require("fs");
22405
- import_path50 = require("path");
22467
+ import_fs50 = require("fs");
22468
+ import_path51 = require("path");
22406
22469
  import_crypto16 = require("crypto");
22407
22470
  import_child_process6 = require("child_process");
22408
22471
  }
@@ -22424,9 +22487,9 @@ async function getLatestVersion() {
22424
22487
  return data.version;
22425
22488
  }
22426
22489
  function detectInstallMethod() {
22427
- const packageRoot = (0, import_path51.resolve)(__dirname, "..", "..", "..", "..");
22490
+ const packageRoot = (0, import_path52.resolve)(__dirname, "..", "..", "..", "..");
22428
22491
  if (process.env.npm_config_global === "true") return "global";
22429
- if ((0, import_fs50.existsSync)((0, import_path51.join)(packageRoot, ".git"))) return "git-clone";
22492
+ if ((0, import_fs51.existsSync)((0, import_path52.join)(packageRoot, ".git"))) return "git-clone";
22430
22493
  return "local";
22431
22494
  }
22432
22495
  function updateCommand(method, version2) {
@@ -22477,7 +22540,7 @@ Check your internet connection or visit https://www.npmjs.com/package/gossipcat
22477
22540
  try {
22478
22541
  (0, import_child_process7.execSync)(command, {
22479
22542
  stdio: "inherit",
22480
- cwd: method === "git-clone" ? (0, import_path51.resolve)(__dirname, "..", "..", "..", "..") : process.cwd()
22543
+ cwd: method === "git-clone" ? (0, import_path52.resolve)(__dirname, "..", "..", "..", "..") : process.cwd()
22481
22544
  });
22482
22545
  } catch (err) {
22483
22546
  return {
@@ -22499,13 +22562,13 @@ Run /mcp reconnect in Claude Code to load the new version.`
22499
22562
  }]
22500
22563
  };
22501
22564
  }
22502
- var import_child_process7, import_fs50, import_path51;
22565
+ var import_child_process7, import_fs51, import_path52;
22503
22566
  var init_gossip_update = __esm({
22504
22567
  "apps/cli/src/handlers/gossip-update.ts"() {
22505
22568
  "use strict";
22506
22569
  import_child_process7 = require("child_process");
22507
- import_fs50 = require("fs");
22508
- import_path51 = require("path");
22570
+ import_fs51 = require("fs");
22571
+ import_path52 = require("path");
22509
22572
  init_version();
22510
22573
  }
22511
22574
  });
@@ -22669,8 +22732,8 @@ var init_verify_memory = __esm({
22669
22732
  });
22670
22733
 
22671
22734
  // apps/cli/src/mcp-server-sdk.ts
22672
- var import_fs51 = require("fs");
22673
- var import_path52 = require("path");
22735
+ var import_fs52 = require("fs");
22736
+ var import_path53 = require("path");
22674
22737
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
22675
22738
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
22676
22739
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
@@ -38030,12 +38093,83 @@ ${gaps.map((g) => ` - ${g.agentId} needs "${g.category}" (score: ${g.score.toFi
38030
38093
 
38031
38094
  // apps/cli/src/mcp-server-sdk.ts
38032
38095
  init_relay_cross_review();
38033
- var gossipDir = (0, import_path52.join)(process.cwd(), ".gossip");
38096
+
38097
+ // apps/cli/src/stickyPort.ts
38098
+ var import_fs34 = require("fs");
38099
+ var import_path36 = require("path");
38100
+ var import_net = require("net");
38101
+ var RELAY_STICKY_FILE = (0, import_path36.join)(".gossip", "relay.port");
38102
+ var HTTP_MCP_STICKY_FILE = (0, import_path36.join)(".gossip", "http-mcp.port");
38103
+ function stickyPath(filename) {
38104
+ return (0, import_path36.join)(process.cwd(), filename);
38105
+ }
38106
+ function readStickyPort(filename) {
38107
+ const p = stickyPath(filename);
38108
+ if (!(0, import_fs34.existsSync)(p)) return null;
38109
+ try {
38110
+ const raw = (0, import_fs34.readFileSync)(p, "utf-8").trim();
38111
+ const n = parseInt(raw, 10);
38112
+ if (!Number.isFinite(n) || n < 1 || n > 65535) return null;
38113
+ return n;
38114
+ } catch {
38115
+ return null;
38116
+ }
38117
+ }
38118
+ function writeStickyPort(filename, port) {
38119
+ if (!Number.isFinite(port) || port < 1 || port > 65535) return;
38120
+ const p = stickyPath(filename);
38121
+ try {
38122
+ (0, import_fs34.mkdirSync)((0, import_path36.dirname)(p), { recursive: true });
38123
+ (0, import_fs34.writeFileSync)(p, String(port), "utf-8");
38124
+ } catch {
38125
+ }
38126
+ }
38127
+ function probePort(port, host = "127.0.0.1") {
38128
+ return new Promise((resolve18) => {
38129
+ const srv = (0, import_net.createServer)();
38130
+ let settled = false;
38131
+ const done = (ok) => {
38132
+ if (settled) return;
38133
+ settled = true;
38134
+ try {
38135
+ srv.close();
38136
+ } catch {
38137
+ }
38138
+ resolve18(ok);
38139
+ };
38140
+ srv.once("error", () => done(false));
38141
+ try {
38142
+ srv.listen(port, host, () => {
38143
+ srv.close(() => done(true));
38144
+ });
38145
+ } catch {
38146
+ done(false);
38147
+ }
38148
+ });
38149
+ }
38150
+ async function pickStickyPort(envVar, stickyFile, probeHost = "127.0.0.1") {
38151
+ const envRaw = process.env[envVar];
38152
+ if (envRaw !== void 0 && envRaw !== "") {
38153
+ const envPort = parseInt(envRaw, 10);
38154
+ if (Number.isFinite(envPort) && envPort >= 0 && envPort <= 65535) {
38155
+ return { port: envPort, source: "env" };
38156
+ }
38157
+ }
38158
+ const sticky = readStickyPort(stickyFile);
38159
+ if (sticky !== null) {
38160
+ const free = await probePort(sticky, probeHost);
38161
+ if (free) return { port: sticky, source: "sticky" };
38162
+ }
38163
+ return { port: 0, source: "auto" };
38164
+ }
38165
+
38166
+ // apps/cli/src/mcp-server-sdk.ts
38167
+ var gossipDir = (0, import_path53.join)(process.cwd(), ".gossip");
38034
38168
  try {
38035
- (0, import_fs51.mkdirSync)(gossipDir, { recursive: true });
38169
+ (0, import_fs52.mkdirSync)(gossipDir, { recursive: true });
38036
38170
  } catch {
38037
38171
  }
38038
- var logStream = (0, import_fs51.createWriteStream)((0, import_path52.join)(gossipDir, "mcp.log"), { flags: "a" });
38172
+ var logStream = (0, import_fs52.createWriteStream)((0, import_path53.join)(gossipDir, "mcp.log"), { flags: "a" });
38039
38173
  process.stderr.write = ((chunk, ...args) => {
38040
38174
  return logStream.write(chunk, ...args);
38041
38175
  });
@@ -38199,17 +38333,18 @@ var booted = false;
38199
38333
  var bootPromise = null;
38200
38334
  var planExecutionDepth = 0;
38201
38335
  var _pendingSessionData = /* @__PURE__ */ new Map();
38336
+ var _pendingSkillData = /* @__PURE__ */ new Map();
38202
38337
  var _pendingVerifyData = /* @__PURE__ */ new Map();
38203
38338
  var _modules = null;
38204
38339
  function lookupFindingSeverity(findingId, projectRoot) {
38205
- const { existsSync: existsSync43, readdirSync: readdirSync13, readFileSync: readFileSync40 } = require("fs");
38206
- const { join: join48 } = require("path");
38207
- const reportsDir = join48(projectRoot, ".gossip", "consensus-reports");
38208
- if (!existsSync43(reportsDir)) return null;
38340
+ const { existsSync: existsSync44, readdirSync: readdirSync13, readFileSync: readFileSync41 } = require("fs");
38341
+ const { join: join49 } = require("path");
38342
+ const reportsDir = join49(projectRoot, ".gossip", "consensus-reports");
38343
+ if (!existsSync44(reportsDir)) return null;
38209
38344
  try {
38210
38345
  const files = readdirSync13(reportsDir).filter((f) => f.endsWith(".json"));
38211
38346
  for (const file2 of files) {
38212
- const report = JSON.parse(readFileSync40(join48(reportsDir, file2), "utf-8"));
38347
+ const report = JSON.parse(readFileSync41(join49(reportsDir, file2), "utf-8"));
38213
38348
  for (const bucket of ["confirmed", "disputed", "unverified", "unique"]) {
38214
38349
  for (const finding of report[bucket] || []) {
38215
38350
  if (finding.id === findingId && finding.severity) {
@@ -38286,7 +38421,7 @@ async function doBoot() {
38286
38421
  const agentConfigs = m.configToAgentConfigs(config2);
38287
38422
  ctx.keychain = new m.Keychain();
38288
38423
  const { existsSync: pidExists, readFileSync: readPid, writeFileSync: writePid, unlinkSync: delPid } = require("fs");
38289
- const pidFile = (0, import_path52.join)(process.cwd(), ".gossip", "relay.pid");
38424
+ const pidFile = (0, import_path53.join)(process.cwd(), ".gossip", "relay.pid");
38290
38425
  if (pidExists(pidFile)) {
38291
38426
  const oldPid = parseInt(readPid(pidFile, "utf-8").trim(), 10);
38292
38427
  if (!isNaN(oldPid) && oldPid !== process.pid) {
@@ -38299,8 +38434,9 @@ async function doBoot() {
38299
38434
  }
38300
38435
  }
38301
38436
  const relayApiKey = (0, import_crypto17.randomBytes)(32).toString("hex");
38302
- const envPort = process.env.GOSSIPCAT_PORT ? parseInt(process.env.GOSSIPCAT_PORT, 10) : NaN;
38303
- const relayPort = Number.isFinite(envPort) && envPort >= 0 && envPort <= 65535 ? envPort : 0;
38437
+ const relayPick = await pickStickyPort("GOSSIPCAT_PORT", RELAY_STICKY_FILE);
38438
+ const relayPort = relayPick.port;
38439
+ ctx.relayPortSource = relayPick.source;
38304
38440
  ctx.relay = new m.RelayServer({
38305
38441
  port: relayPort,
38306
38442
  apiKey: relayApiKey,
@@ -38310,7 +38446,10 @@ async function doBoot() {
38310
38446
  }
38311
38447
  });
38312
38448
  await ctx.relay.start();
38313
- startHttpMcpTransport();
38449
+ if (typeof ctx.relay.port === "number" && ctx.relay.port > 0) {
38450
+ writeStickyPort(RELAY_STICKY_FILE, ctx.relay.port);
38451
+ }
38452
+ await startHttpMcpTransport();
38314
38453
  try {
38315
38454
  writePid(pidFile, String(process.pid));
38316
38455
  } catch {
@@ -38376,10 +38515,10 @@ async function doBoot() {
38376
38515
  }
38377
38516
  const key = await ctx.keychain.getKey(ac.provider);
38378
38517
  const llm = m.createProvider(ac.provider, ac.model, key ?? void 0, void 0, ac.base_url);
38379
- const { existsSync: existsSync43, readFileSync: readFileSync40 } = require("fs");
38380
- const { join: join48 } = require("path");
38381
- const instructionsPath = join48(process.cwd(), ".gossip", "agents", ac.id, "instructions.md");
38382
- const baseInstructions = existsSync43(instructionsPath) ? readFileSync40(instructionsPath, "utf-8") : "";
38518
+ const { existsSync: existsSync44, readFileSync: readFileSync41 } = require("fs");
38519
+ const { join: join49 } = require("path");
38520
+ const instructionsPath = join49(process.cwd(), ".gossip", "agents", ac.id, "instructions.md");
38521
+ const baseInstructions = existsSync44(instructionsPath) ? readFileSync41(instructionsPath, "utf-8") : "";
38383
38522
  const identity = identityRegistry.get(ac.id);
38384
38523
  const identityBlock = identity ? m.formatIdentityBlock(identity) + "\n" : "";
38385
38524
  const instructions = (identityBlock + baseInstructions).trim() || void 0;
@@ -38940,18 +39079,21 @@ server.tool(
38940
39079
  "Status:",
38941
39080
  ` Host: ${env.host}${env.supportsNativeAgents ? " (native agents supported)" : ""}`,
38942
39081
  ` Native agent dir: ${env.nativeAgentDir || "n/a"}`,
38943
- ` Relay: ${ctx.relay ? `running :${ctx.relay.port}` : "not started"}`,
39082
+ ` Relay: ${ctx.relay ? `running :${ctx.relay.port}${ctx.relayPortSource === "sticky" ? " (sticky)" : ""}` : "not started"}`,
38944
39083
  ` Tool Server: ${ctx.toolServer ? "running" : "not started"}`,
38945
39084
  ` Workers: ${ctx.workers.size} (${Array.from(ctx.workers.keys()).join(", ") || "none"})`,
38946
39085
  ` Claude subagents found: ${claudeSubagentsList.length}`
38947
39086
  ];
38948
39087
  if (ctx.relay?.dashboardUrl) {
38949
- lines.push(` Dashboard: ${ctx.relay.dashboardUrl} (key: ${ctx.relay.dashboardKey})`);
39088
+ lines.push(` Dashboard: ${ctx.relay.dashboardUrl}${ctx.relayPortSource === "sticky" ? " (sticky)" : ""} (key: ${ctx.relay.dashboardKey})`);
39089
+ }
39090
+ if (ctx.httpMcpPort) {
39091
+ lines.push(` HTTP MCP: :${ctx.httpMcpPort}/mcp${ctx.httpMcpPortSource === "sticky" ? " (sticky)" : ""}`);
38950
39092
  }
38951
39093
  try {
38952
- const { readFileSync: readFileSync40 } = await import("fs");
38953
- const quotaPath = (0, import_path52.join)(process.cwd(), ".gossip", "quota-state.json");
38954
- const quotaRaw = readFileSync40(quotaPath, "utf8");
39094
+ const { readFileSync: readFileSync41 } = await import("fs");
39095
+ const quotaPath = (0, import_path53.join)(process.cwd(), ".gossip", "quota-state.json");
39096
+ const quotaRaw = readFileSync41(quotaPath, "utf8");
38955
39097
  const quotaState = JSON.parse(quotaRaw);
38956
39098
  for (const [provider, state] of Object.entries(quotaState)) {
38957
39099
  const now = Date.now();
@@ -38965,16 +39107,16 @@ server.tool(
38965
39107
  } catch {
38966
39108
  }
38967
39109
  try {
38968
- const { readFileSync: readFileSync40, readdirSync: readdirSync13, statSync: statSync9 } = await import("fs");
38969
- const reportsDir = (0, import_path52.join)(process.cwd(), ".gossip", "consensus-reports");
38970
- const perfPath = (0, import_path52.join)(process.cwd(), ".gossip", "agent-performance.jsonl");
39110
+ const { readFileSync: readFileSync41, readdirSync: readdirSync13, statSync: statSync9 } = await import("fs");
39111
+ const reportsDir = (0, import_path53.join)(process.cwd(), ".gossip", "consensus-reports");
39112
+ const perfPath = (0, import_path53.join)(process.cwd(), ".gossip", "agent-performance.jsonl");
38971
39113
  const WINDOW_MS = 24 * 60 * 60 * 1e3;
38972
39114
  const now = Date.now();
38973
39115
  const recentReports = [];
38974
39116
  try {
38975
39117
  for (const fname of readdirSync13(reportsDir)) {
38976
39118
  if (!fname.endsWith(".json")) continue;
38977
- const fpath = (0, import_path52.join)(reportsDir, fname);
39119
+ const fpath = (0, import_path53.join)(reportsDir, fname);
38978
39120
  const st = statSync9(fpath);
38979
39121
  if (now - st.mtimeMs > WINDOW_MS) continue;
38980
39122
  recentReports.push({ id: fname.replace(/\.json$/, ""), mtimeMs: st.mtimeMs });
@@ -38984,7 +39126,7 @@ server.tool(
38984
39126
  if (recentReports.length > 0) {
38985
39127
  const covered = /* @__PURE__ */ new Set();
38986
39128
  try {
38987
- const perfRaw = readFileSync40(perfPath, "utf8");
39129
+ const perfRaw = readFileSync41(perfPath, "utf8");
38988
39130
  for (const line of perfRaw.split("\n")) {
38989
39131
  if (!line) continue;
38990
39132
  try {
@@ -39116,7 +39258,73 @@ ${toolsMatch[1].trim()}`;
39116
39258
  }
39117
39259
  } catch {
39118
39260
  }
39119
- return { content: [{ type: "text", text: lines.join("\n") + "\n\n" + agentSections.join("\n") + sessionContextSection }] };
39261
+ let handbookSection = "";
39262
+ try {
39263
+ const { readFileSync: rfHB, statSync: stHB } = require("fs");
39264
+ const { join: jHB } = require("path");
39265
+ const handbookPath = jHB(process.cwd(), "docs", "HANDBOOK.md");
39266
+ const stat3 = stHB(handbookPath);
39267
+ const HANDBOOK_CAP_BYTES = 24 * 1024;
39268
+ let body = rfHB(handbookPath, "utf-8");
39269
+ const truncated = body.length > HANDBOOK_CAP_BYTES;
39270
+ if (truncated) {
39271
+ body = body.slice(0, HANDBOOK_CAP_BYTES);
39272
+ }
39273
+ handbookSection = "\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n## Project Handbook (auto-loaded from docs/HANDBOOK.md)\n\n" + body.trim() + (truncated ? `
39274
+
39275
+ [handbook truncated at ${HANDBOOK_CAP_BYTES / 1024}KB \u2014 full file at docs/HANDBOOK.md, ${stat3.size} bytes total]` : "");
39276
+ } catch {
39277
+ }
39278
+ return { content: [{ type: "text", text: lines.join("\n") + "\n\n" + agentSections.join("\n") + sessionContextSection + handbookSection }] };
39279
+ }
39280
+ );
39281
+ server.tool(
39282
+ "gossip_guide",
39283
+ "Show the gossipcat handbook for humans \u2014 architectural invariants, operator playbook, caveats, hallucination patterns, glossary. Call this when you want to READ the docs, not when you need LLM context. The LLM-facing auto-load lives in gossip_status().",
39284
+ {},
39285
+ async () => {
39286
+ try {
39287
+ const { readFileSync: rfG, existsSync: exG } = require("fs");
39288
+ const { join: jG, dirname: dG } = require("path");
39289
+ const projectHandbook = jG(process.cwd(), "docs", "HANDBOOK.md");
39290
+ if (exG(projectHandbook)) {
39291
+ const body = rfG(projectHandbook, "utf-8");
39292
+ return {
39293
+ content: [{
39294
+ type: "text",
39295
+ text: `# Gossipcat Handbook (from ${projectHandbook})
39296
+
39297
+ ${body}`
39298
+ }]
39299
+ };
39300
+ }
39301
+ const bundleRoot = dG(require.resolve("../../dist-mcp/mcp-server.js").replace(/\/dist-mcp\/mcp-server\.js$/, "/dist-mcp/mcp-server.js"));
39302
+ const defaultHandbook = jG(bundleRoot, "..", "docs", "HANDBOOK.md");
39303
+ if (exG(defaultHandbook)) {
39304
+ const body = rfG(defaultHandbook, "utf-8");
39305
+ return {
39306
+ content: [{
39307
+ type: "text",
39308
+ text: `# Gossipcat Handbook (bundled default \u2014 your project has no docs/HANDBOOK.md yet)
39309
+
39310
+ ${body}`
39311
+ }]
39312
+ };
39313
+ }
39314
+ return {
39315
+ content: [{
39316
+ type: "text",
39317
+ text: "No handbook found. Expected at docs/HANDBOOK.md in the current project or bundled with gossipcat. If you are inside a gossipcat project, run `gossip_setup` first."
39318
+ }]
39319
+ };
39320
+ } catch (err) {
39321
+ return {
39322
+ content: [{
39323
+ type: "text",
39324
+ text: `Error reading handbook: ${err.message}`
39325
+ }]
39326
+ };
39327
+ }
39120
39328
  }
39121
39329
  );
39122
39330
  server.tool(
@@ -39223,8 +39431,8 @@ server.tool(
39223
39431
  }
39224
39432
  return { content: [{ type: "text", text: results.join("\n") }] };
39225
39433
  }
39226
- const { writeFileSync: writeFileSync17, mkdirSync: mkdirSync21, existsSync: existsSync43 } = require("fs");
39227
- const { join: join48 } = require("path");
39434
+ const { writeFileSync: writeFileSync18, mkdirSync: mkdirSync22, existsSync: existsSync44 } = require("fs");
39435
+ const { join: join49 } = require("path");
39228
39436
  const root = process.cwd();
39229
39437
  const CLAUDE_MODEL_MAP2 = {
39230
39438
  opus: { provider: "anthropic", model: "claude-opus-4-6" },
@@ -39238,8 +39446,8 @@ server.tool(
39238
39446
  let existingAgents = {};
39239
39447
  if (mode === "merge") {
39240
39448
  try {
39241
- const { readFileSync: readFileSync40 } = require("fs");
39242
- const existing = JSON.parse(readFileSync40(join48(root, ".gossip", "config.json"), "utf-8"));
39449
+ const { readFileSync: readFileSync41 } = require("fs");
39450
+ const existing = JSON.parse(readFileSync41(join49(root, ".gossip", "config.json"), "utf-8"));
39243
39451
  existingAgents = existing.agents || {};
39244
39452
  } catch {
39245
39453
  }
@@ -39271,9 +39479,9 @@ server.tool(
39271
39479
  "",
39272
39480
  body
39273
39481
  ].join("\n");
39274
- const agentsDir = join48(root, ".claude", "agents");
39275
- mkdirSync21(agentsDir, { recursive: true });
39276
- writeFileSync17(join48(agentsDir, `${agent.id}.md`), md, "utf-8");
39482
+ const agentsDir = join49(root, ".claude", "agents");
39483
+ mkdirSync22(agentsDir, { recursive: true });
39484
+ writeFileSync18(join49(agentsDir, `${agent.id}.md`), md, "utf-8");
39277
39485
  nativeCreated.push(agent.id);
39278
39486
  configAgents[agent.id] = {
39279
39487
  provider: mapped.provider,
@@ -39291,8 +39499,8 @@ server.tool(
39291
39499
  errors.push(`${agent.id}: custom agent requires "custom_model" field`);
39292
39500
  continue;
39293
39501
  }
39294
- const nativeFile = join48(root, ".claude", "agents", `${agent.id}.md`);
39295
- const wasNative = existingAgents[agent.id]?.native || existsSync43(nativeFile);
39502
+ const nativeFile = join49(root, ".claude", "agents", `${agent.id}.md`);
39503
+ const wasNative = existingAgents[agent.id]?.native || existsSync44(nativeFile);
39296
39504
  if (wasNative) {
39297
39505
  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.`);
39298
39506
  continue;
@@ -39306,9 +39514,9 @@ server.tool(
39306
39514
  };
39307
39515
  customCreated.push(agent.id);
39308
39516
  if (agent.instructions) {
39309
- const instrDir = join48(root, ".gossip", "agents", agent.id);
39310
- mkdirSync21(instrDir, { recursive: true });
39311
- writeFileSync17(join48(instrDir, "instructions.md"), agent.instructions, "utf-8");
39517
+ const instrDir = join49(root, ".gossip", "agents", agent.id);
39518
+ mkdirSync22(instrDir, { recursive: true });
39519
+ writeFileSync18(join49(instrDir, "instructions.md"), agent.instructions, "utf-8");
39312
39520
  }
39313
39521
  }
39314
39522
  }
@@ -39322,13 +39530,13 @@ server.tool(
39322
39530
  } catch (err) {
39323
39531
  return { content: [{ type: "text", text: `Invalid config: ${err.message}` }] };
39324
39532
  }
39325
- mkdirSync21(join48(root, ".gossip"), { recursive: true });
39326
- writeFileSync17(join48(root, ".gossip", "config.json"), JSON.stringify(config2, null, 2));
39533
+ mkdirSync22(join49(root, ".gossip"), { recursive: true });
39534
+ writeFileSync18(join49(root, ".gossip", "config.json"), JSON.stringify(config2, null, 2));
39327
39535
  const agentList = Object.entries(config2.agents).map(([id, a]) => `- ${id}: ${a.provider}/${a.model} (${a.preset || "custom"})${a.native ? " \u2014 native" : ""}`).join("\n");
39328
- const rulesDir = join48(root, env.rulesDir);
39329
- const rulesFile = join48(root, env.rulesFile);
39330
- mkdirSync21(rulesDir, { recursive: true });
39331
- writeFileSync17(rulesFile, generateRulesContent(agentList));
39536
+ const rulesDir = join49(root, env.rulesDir);
39537
+ const rulesFile = join49(root, env.rulesFile);
39538
+ mkdirSync22(rulesDir, { recursive: true });
39539
+ writeFileSync18(rulesFile, generateRulesContent(agentList));
39332
39540
  const lines = [`Host: ${env.host}`, ""];
39333
39541
  if (nativeCreated.length > 0) {
39334
39542
  lines.push(`Native agents created (${nativeCreated.length}):`);
@@ -40034,9 +40242,11 @@ server.tool(
40034
40242
  skills: external_exports.array(external_exports.object({
40035
40243
  name: external_exports.string().describe("Skill name (kebab-case)"),
40036
40244
  content: external_exports.string().describe("Full .md content with frontmatter")
40037
- })).optional().describe("For build: generated skill files to save. Omit for discovery mode.")
40245
+ })).optional().describe("For build: generated skill files to save. Omit for discovery mode."),
40246
+ // Internal: re-entry param for native-utility dispatch path (develop action only)
40247
+ _utility_task_id: external_exports.string().optional().describe("Internal: utility task ID for re-entry after native skill generation")
40038
40248
  },
40039
- async ({ action, agent_id, skill, enabled, category, skill_names, skills }) => {
40249
+ async ({ action, agent_id, skill, enabled, category, skill_names, skills, _utility_task_id }) => {
40040
40250
  await boot();
40041
40251
  if (action === "list") {
40042
40252
  const index = ctx.mainAgent.getSkillIndex();
@@ -40092,6 +40302,98 @@ ${sections.join("\n\n")}` }] };
40092
40302
  if (!ctx.skillEngine) {
40093
40303
  return { content: [{ type: "text", text: "Skill engine not available. Check boot logs." }] };
40094
40304
  }
40305
+ if (_utility_task_id) {
40306
+ const stashedMeta = _pendingSkillData.get(_utility_task_id);
40307
+ _pendingSkillData.delete(_utility_task_id);
40308
+ const utilityResult = ctx.nativeResultMap.get(_utility_task_id);
40309
+ ctx.nativeResultMap.delete(_utility_task_id);
40310
+ ctx.nativeTaskMap.delete(_utility_task_id);
40311
+ if (utilityResult?.status === "completed" && utilityResult.result && stashedMeta) {
40312
+ try {
40313
+ const result = ctx.skillEngine.saveFromRaw(agent_id, category, utilityResult.result, stashedMeta);
40314
+ const { normalizeSkillName: nsn } = await Promise.resolve().then(() => (init_src4(), src_exports3));
40315
+ const skillName = nsn(category);
40316
+ if (ctx.mainAgent) {
40317
+ const skillIndex = ctx.mainAgent.getSkillIndex();
40318
+ if (skillIndex) {
40319
+ skillIndex.bind(agent_id, skillName, { source: "auto", mode: "permanent" });
40320
+ }
40321
+ const registry2 = ctx.mainAgent.registry;
40322
+ const agentConfig = registry2?.get(agent_id);
40323
+ const normalizedCategory = nsn(category);
40324
+ if (agentConfig && !agentConfig.skills.includes(normalizedCategory)) {
40325
+ agentConfig.skills.push(normalizedCategory);
40326
+ }
40327
+ const pipeline = ctx.mainAgent.pipeline;
40328
+ if (pipeline?.suppressSkillGapAlert) {
40329
+ pipeline.suppressSkillGapAlert(agent_id, category);
40330
+ }
40331
+ }
40332
+ const preview = result.content.length > 1e3 ? result.content.slice(0, 1e3) + "\n\n... (truncated)" : result.content;
40333
+ process.stderr.write(`[gossipcat] Skill developed (native): "${skillName}" for ${agent_id} (category: ${category})
40334
+ `);
40335
+ return {
40336
+ content: [{ type: "text", text: `Skill generated and saved:
40337
+
40338
+ Path: ${result.path}
40339
+
40340
+ Auto-bound "${skillName}" to ${agent_id} in skill index.
40341
+
40342
+ ${preview}` }]
40343
+ };
40344
+ } catch (err) {
40345
+ process.stderr.write(`[gossipcat] Skill develop native post-processing failed: ${err.message}
40346
+ `);
40347
+ return {
40348
+ content: [{ type: "text", text: `Skill generation failed: ${err.message}` }]
40349
+ };
40350
+ }
40351
+ } else {
40352
+ process.stderr.write(`[gossipcat] Skill develop utility ${_utility_task_id} failed/missing, falling back to direct LLM
40353
+ `);
40354
+ }
40355
+ }
40356
+ if (ctx.nativeUtilityConfig && !_utility_task_id) {
40357
+ try {
40358
+ const { system, user, skillName, skillPath, baseline_accuracy_correct, baseline_accuracy_hallucinated, bound_at } = await ctx.skillEngine.buildPrompt(agent_id, category);
40359
+ const taskId = (0, import_crypto17.randomUUID)().slice(0, 8);
40360
+ _pendingSkillData.set(taskId, { agentId: agent_id, category, skillName, skillPath, baseline_accuracy_correct, baseline_accuracy_hallucinated, bound_at });
40361
+ ctx.nativeTaskMap.set(taskId, {
40362
+ agentId: "_utility",
40363
+ task: `skill_develop:${category}`,
40364
+ startedAt: Date.now(),
40365
+ timeoutMs: 12e4,
40366
+ utilityType: "skill_develop"
40367
+ });
40368
+ spawnTimeoutWatcher(taskId, ctx.nativeTaskMap.get(taskId));
40369
+ const modelShort = ctx.nativeUtilityConfig.model;
40370
+ return {
40371
+ content: [
40372
+ {
40373
+ type: "text",
40374
+ text: `Skill prompt built. Dispatching native utility for generation.
40375
+
40376
+ \u26A0\uFE0F EXECUTE NOW \u2014 launch this Agent and re-call gossip_skills:
40377
+
40378
+ 1. Agent(model: "${modelShort}", prompt: <AGENT_PROMPT:${taskId} below>, run_in_background: true) \u2014 pass the AGENT_PROMPT:${taskId} content item verbatim
40379
+ 2. When agent completes \u2192 gossip_relay(task_id: "${taskId}", result: "<full agent output>")
40380
+ 3. Then re-call: gossip_skills(action: "develop", agent_id: "${agent_id}", category: "${category}", _utility_task_id: "${taskId}")
40381
+
40382
+ Do ALL steps in order. Do not wait for user input between them.`
40383
+ },
40384
+ { type: "text", text: `AGENT_PROMPT:${taskId} (_utility)
40385
+ ${system}
40386
+
40387
+ ---
40388
+
40389
+ ${user}` }
40390
+ ]
40391
+ };
40392
+ } catch (err) {
40393
+ process.stderr.write(`[gossipcat] Skill develop native prompt build failed: ${err.message}, falling back to direct LLM
40394
+ `);
40395
+ }
40396
+ }
40095
40397
  try {
40096
40398
  const result = await ctx.skillEngine.generate(agent_id, category);
40097
40399
  const { normalizeSkillName: nsn } = await Promise.resolve().then(() => (init_src4(), src_exports3));
@@ -40135,16 +40437,16 @@ ${preview}` }]
40135
40437
  const { SkillGapTracker: SkillGapTracker2, parseSkillFrontmatter: parseSkillFrontmatter2, normalizeSkillName: normalizeSkillName2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
40136
40438
  const tracker = new SkillGapTracker2(process.cwd());
40137
40439
  if (skills && skills.length > 0) {
40138
- const { writeFileSync: writeFileSync17, mkdirSync: mkdirSync21, existsSync: existsSync43, readFileSync: readFileSync40 } = require("fs");
40139
- const { join: join48 } = require("path");
40140
- const dir = join48(process.cwd(), ".gossip", "skills");
40141
- mkdirSync21(dir, { recursive: true });
40440
+ const { writeFileSync: writeFileSync18, mkdirSync: mkdirSync22, existsSync: existsSync44, readFileSync: readFileSync41 } = require("fs");
40441
+ const { join: join49 } = require("path");
40442
+ const dir = join49(process.cwd(), ".gossip", "skills");
40443
+ mkdirSync22(dir, { recursive: true });
40142
40444
  const results = [];
40143
40445
  for (const sk of skills) {
40144
40446
  const name = normalizeSkillName2(sk.name);
40145
- const filePath = join48(dir, `${name}.md`);
40146
- if (existsSync43(filePath)) {
40147
- const existing = readFileSync40(filePath, "utf-8");
40447
+ const filePath = join49(dir, `${name}.md`);
40448
+ if (existsSync44(filePath)) {
40449
+ const existing = readFileSync41(filePath, "utf-8");
40148
40450
  const fm = parseSkillFrontmatter2(existing);
40149
40451
  if (fm) {
40150
40452
  if (fm.generated_by === "manual") {
@@ -40161,7 +40463,7 @@ ${preview}` }]
40161
40463
  }
40162
40464
  }
40163
40465
  }
40164
- writeFileSync17(filePath, sk.content);
40466
+ writeFileSync18(filePath, sk.content);
40165
40467
  tracker.recordResolution(name);
40166
40468
  results.push(`Created .gossip/skills/${name}.md`);
40167
40469
  }
@@ -40205,9 +40507,10 @@ server.tool(
40205
40507
  "Save a cognitive session summary to project memory. The next session will load this context automatically on MCP connect. Call before ending your session to preserve what was learned.",
40206
40508
  {
40207
40509
  notes: external_exports.string().optional().describe('Optional freeform user context (e.g., "focusing on security hardening")'),
40510
+ force: external_exports.boolean().optional().describe("Bypass the refuse-gate for pending native tasks / consensus rounds. Audited to .gossip/forced-saves.jsonl."),
40208
40511
  _utility_task_id: external_exports.string().optional().describe("Internal: utility task ID for re-entry after native session summary")
40209
40512
  },
40210
- async ({ notes, _utility_task_id }) => {
40513
+ async ({ notes, force, _utility_task_id }) => {
40211
40514
  await boot();
40212
40515
  if (_utility_task_id) {
40213
40516
  const stashed = _pendingSessionData.get(_utility_task_id);
@@ -40308,6 +40611,48 @@ server.tool(
40308
40611
 
40309
40612
  ${summary2}` }] };
40310
40613
  }
40614
+ {
40615
+ const pendingNative = [];
40616
+ for (const [taskId, info] of ctx.nativeTaskMap) {
40617
+ if (!ctx.nativeResultMap.has(taskId)) pendingNative.push(`${taskId} (${info.agentId})`);
40618
+ }
40619
+ const pendingConsensus = [];
40620
+ const now = Date.now();
40621
+ for (const [cid, round] of ctx.pendingConsensusRounds) {
40622
+ if (round.deadline && now > round.deadline) continue;
40623
+ if (round.pendingNativeAgents && round.pendingNativeAgents.size > 0) {
40624
+ pendingConsensus.push(`${cid} (${round.pendingNativeAgents.size} agents)`);
40625
+ }
40626
+ }
40627
+ if ((pendingNative.length > 0 || pendingConsensus.length > 0) && !force) {
40628
+ const lines = ["\u26D4 session_save refused \u2014 work still in flight.\n"];
40629
+ if (pendingNative.length > 0) {
40630
+ lines.push(`Pending native tasks (${pendingNative.length}):`);
40631
+ for (const t of pendingNative.slice(0, 10)) lines.push(` - ${t}`);
40632
+ lines.push("");
40633
+ }
40634
+ if (pendingConsensus.length > 0) {
40635
+ lines.push(`Pending consensus rounds (${pendingConsensus.length}):`);
40636
+ for (const c of pendingConsensus.slice(0, 10)) lines.push(` - ${c}`);
40637
+ lines.push("");
40638
+ }
40639
+ lines.push("Relay the results via gossip_relay / gossip_relay_cross_review, then re-call gossip_session_save.");
40640
+ lines.push("To override (snapshot is intentional / tasks are abandoned): gossip_session_save(force: true).");
40641
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: true };
40642
+ }
40643
+ if (force && (pendingNative.length > 0 || pendingConsensus.length > 0)) {
40644
+ try {
40645
+ const { appendFileSync: af, mkdirSync: md } = require("fs");
40646
+ const { join: j } = require("path");
40647
+ md(j(process.cwd(), ".gossip"), { recursive: true });
40648
+ af(
40649
+ j(process.cwd(), ".gossip", "forced-saves.jsonl"),
40650
+ JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), pendingNative, pendingConsensus, notes: notes || null }) + "\n"
40651
+ );
40652
+ } catch {
40653
+ }
40654
+ }
40655
+ }
40311
40656
  let gossipText = "";
40312
40657
  try {
40313
40658
  const { existsSync: ex, readFileSync: rf } = require("fs");
@@ -40677,6 +41022,7 @@ server.tool(
40677
41022
  { name: "gossip_remember", desc: "Search an agent's archived knowledge files by keyword query." },
40678
41023
  { name: "gossip_verify_memory", desc: "On-demand staleness check for a memory file claim. Returns FRESH | STALE | CONTRADICTED | INCONCLUSIVE with file:line evidence." },
40679
41024
  { name: "gossip_tools", desc: "List available tools (this command)." },
41025
+ { name: "gossip_guide", desc: "Show the gossipcat handbook for humans \u2014 invariants, operator playbook, caveats, hallucination patterns, glossary. Read the docs, not LLM context." },
40680
41026
  { name: "gossip_progress", desc: "Show active task progress and consensus phase. No params." },
40681
41027
  { name: "gossip_format", desc: "Return the CONSENSUS_OUTPUT_FORMAT block to paste into ad-hoc Agent() prompts so native subagents emit parseable <agent_finding> tags." },
40682
41028
  { name: "gossip_bug_feedback", desc: "File a GitHub issue on the gossipcat repo from an in-session bug report. Dedupes against open issues." }
@@ -40888,8 +41234,10 @@ function touchSession(sid) {
40888
41234
  `);
40889
41235
  }, HTTP_SESSION_TTL_MS);
40890
41236
  }
40891
- function startHttpMcpTransport() {
40892
- const port = parseInt(process.env.GOSSIPCAT_HTTP_PORT ?? "24421", 10);
41237
+ async function startHttpMcpTransport() {
41238
+ const httpPick = await pickStickyPort("GOSSIPCAT_HTTP_PORT", HTTP_MCP_STICKY_FILE);
41239
+ const port = httpPick.source === "auto" ? 24421 : httpPick.port;
41240
+ ctx.httpMcpPortSource = httpPick.source;
40893
41241
  const token = process.env.GOSSIPCAT_HTTP_TOKEN ?? "";
40894
41242
  const bindHost = process.env.GOSSIPCAT_HTTP_BIND === "0.0.0.0" && token ? "0.0.0.0" : "127.0.0.1";
40895
41243
  const tokenBuf = token ? Buffer.from(token) : null;
@@ -40983,9 +41331,13 @@ function startHttpMcpTransport() {
40983
41331
  res.end(JSON.stringify({ error: "Bad request" }));
40984
41332
  });
40985
41333
  httpServer.listen(port, bindHost, () => {
41334
+ const addr = httpServer.address();
41335
+ const bound = typeof addr === "object" && addr ? addr.port : port;
41336
+ ctx.httpMcpPort = bound;
41337
+ if (bound > 0) writeStickyPort(HTTP_MCP_STICKY_FILE, bound);
40986
41338
  const authNote = token ? " (token protected)" : " (no auth \u2014 set GOSSIPCAT_HTTP_TOKEN to secure)";
40987
41339
  const bindNote = bindHost === "0.0.0.0" ? " [remote]" : " [localhost only]";
40988
- process.stderr.write(`[gossipcat] HTTP MCP listening on :${port}/mcp${authNote}${bindNote}
41340
+ process.stderr.write(`[gossipcat] HTTP MCP listening on :${bound}/mcp${authNote}${bindNote}
40989
41341
  `);
40990
41342
  });
40991
41343
  httpServer.on("error", (err) => {