gossipcat 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist-mcp/mcp-server.js +478 -284
  2. package/package.json +1 -1
@@ -52,9 +52,13 @@ var init_mcp_context = __esm({
52
52
  nativeTaskMap: /* @__PURE__ */ new Map(),
53
53
  nativeResultMap: /* @__PURE__ */ new Map(),
54
54
  nativeAgentConfigs: /* @__PURE__ */ new Map(),
55
+ identityRegistry: /* @__PURE__ */ new Map(),
55
56
  pendingConsensusRounds: /* @__PURE__ */ new Map(),
56
57
  nativeUtilityConfig: null,
57
58
  mainProvider: "google",
59
+ mainModel: "gemini-2.5-pro",
60
+ mainProviderConfig: "google",
61
+ mainModelConfig: "gemini-2.5-pro",
58
62
  httpMcpPort: null,
59
63
  relayPortSource: null,
60
64
  httpMcpPortSource: null,
@@ -795,10 +799,15 @@ var init_task_dispatcher = __esm({
795
799
  * Decompose a task into a DispatchPlan using the LLM.
796
800
  * On parse failure, falls back to a single sub-task.
797
801
  */
798
- async decompose(task) {
802
+ /**
803
+ * Build the LLM messages used by decompose(). Exposed so native-utility
804
+ * orchestrators (Claude Code) can dispatch the decomposition as an Agent()
805
+ * call and feed the raw result back through decomposeFromRaw().
806
+ */
807
+ buildDecomposeMessages(task) {
799
808
  const availableSkills = this.getAvailableSkills();
800
809
  const skillList = availableSkills.length > 0 ? availableSkills.join(", ") : "general";
801
- const messages = [
810
+ return [
802
811
  {
803
812
  role: "system",
804
813
  content: `You are a task decomposition engine. Break work into tasks that use the FULL team.
@@ -834,36 +843,55 @@ Use "sequential" ONLY when a later task genuinely needs output from an earlier o
834
843
  },
835
844
  { role: "user", content: task }
836
845
  ];
837
- const response = await this.llm.generate(messages, { temperature: 0 });
846
+ }
847
+ /**
848
+ * Parse a raw LLM response into a DispatchPlan. Used by both the in-process
849
+ * decompose() path and the native-utility re-entry path — same fallback
850
+ * logic, one place to keep it honest.
851
+ *
852
+ * Strategy/subtask shape validation was previously implicit (trusted-LLM
853
+ * output). The native-utility path feeds raw subagent output through here
854
+ * untrusted, so we validate explicitly: unknown strategies fall back to
855
+ * 'single', non-string descriptions or missing description fields skip the
856
+ * subtask, and if nothing survives we fall through to the single-task
857
+ * default. This is F17 hardening from consensus 0a7c34cb-91624bd4.
858
+ */
859
+ decomposeFromRaw(task, rawText) {
860
+ const VALID_STRATEGIES = /* @__PURE__ */ new Set(["single", "parallel", "sequential"]);
861
+ const singleTaskFallback = () => ({
862
+ originalTask: task,
863
+ strategy: "single",
864
+ subTasks: [{
865
+ id: (0, import_crypto2.randomUUID)(),
866
+ description: task,
867
+ requiredSkills: [],
868
+ status: "pending"
869
+ }],
870
+ warnings: []
871
+ });
838
872
  try {
839
- const jsonMatch = response.text.match(/\{[\s\S]*\}/);
873
+ const jsonMatch = rawText.match(/\{[\s\S]*\}/);
840
874
  if (!jsonMatch) throw new Error("No JSON in response");
841
875
  const plan = JSON.parse(jsonMatch[0]);
842
- return {
843
- originalTask: task,
844
- strategy: plan.strategy || "single",
845
- subTasks: (plan.subTasks || []).map((st) => ({
846
- id: (0, import_crypto2.randomUUID)(),
847
- description: st.description,
848
- requiredSkills: st.requiredSkills || [],
849
- status: "pending"
850
- })),
851
- warnings: []
852
- };
876
+ const strategy = VALID_STRATEGIES.has(plan.strategy) ? plan.strategy : "single";
877
+ const rawSubTasks = Array.isArray(plan.subTasks) ? plan.subTasks : [];
878
+ const subTasks = rawSubTasks.filter((st) => st && typeof st.description === "string" && st.description.trim().length > 0).map((st) => ({
879
+ id: (0, import_crypto2.randomUUID)(),
880
+ description: st.description,
881
+ requiredSkills: Array.isArray(st.requiredSkills) ? st.requiredSkills.filter((s) => typeof s === "string") : [],
882
+ status: "pending"
883
+ }));
884
+ if (subTasks.length === 0) return singleTaskFallback();
885
+ return { originalTask: task, strategy, subTasks, warnings: [] };
853
886
  } catch {
854
- return {
855
- originalTask: task,
856
- strategy: "single",
857
- subTasks: [{
858
- id: (0, import_crypto2.randomUUID)(),
859
- description: task,
860
- requiredSkills: [],
861
- status: "pending"
862
- }],
863
- warnings: []
864
- };
887
+ return singleTaskFallback();
865
888
  }
866
889
  }
890
+ async decompose(task) {
891
+ const messages = this.buildDecomposeMessages(task);
892
+ const response = await this.llm.generate(messages, { temperature: 0 });
893
+ return this.decomposeFromRaw(task, response.text);
894
+ }
867
895
  /**
868
896
  * Assign agents to each sub-task by skill match.
869
897
  * Modifies the plan in-place and returns it.
@@ -935,13 +963,22 @@ ${subTaskList}` }
935
963
  };
936
964
  });
937
965
  } catch {
938
- return plan.subTasks.map((st) => ({
939
- agentId: st.assignedAgent || "",
940
- task: st.description,
941
- access: "read"
942
- }));
966
+ return this.classifyWriteModesFallback(plan);
943
967
  }
944
968
  }
969
+ /**
970
+ * All-read fallback mapping used when no LLM is available (e.g. pure-native
971
+ * teams using the native-utility path for decomposition but lacking a
972
+ * second round-trip budget for classification). Also used internally by
973
+ * classifyWriteModes() on LLM failure.
974
+ */
975
+ classifyWriteModesFallback(plan) {
976
+ return plan.subTasks.map((st) => ({
977
+ agentId: st.assignedAgent || "",
978
+ task: st.description,
979
+ access: "read"
980
+ }));
981
+ }
945
982
  /** Collect all unique skills from registered agents */
946
983
  getAvailableSkills() {
947
984
  const skills = /* @__PURE__ */ new Set();
@@ -9537,57 +9574,66 @@ Before completing:
9537
9574
  3. Confirm referenced functions/methods exist${fileList}`;
9538
9575
  }
9539
9576
  function assemblePrompt(parts) {
9540
- const blocks = [];
9577
+ const prefix = [];
9578
+ const suffix = [];
9579
+ if (parts.identity) {
9580
+ prefix.push(parts.identity);
9581
+ }
9541
9582
  if (parts.projectStructure) {
9542
- blocks.push(`
9583
+ prefix.push(`
9543
9584
 
9544
9585
  --- PROJECT ---
9545
9586
  ${parts.projectStructure}
9546
9587
  --- END PROJECT ---`);
9547
9588
  }
9548
9589
  if (parts.chainContext) {
9549
- blocks.push(`
9590
+ prefix.push(`
9550
9591
 
9551
9592
  ${parts.chainContext}`);
9593
+ }
9594
+ if (parts.instructions) {
9595
+ prefix.push(`
9596
+
9597
+ ${parts.instructions}`);
9552
9598
  }
9553
9599
  if (parts.skills) {
9554
- blocks.push(`
9600
+ prefix.push(`
9555
9601
 
9556
9602
  --- SKILLS ---
9557
9603
  ${parts.skills}
9558
9604
  --- END SKILLS ---`);
9559
9605
  }
9560
9606
  if (parts.consensusSummary) {
9561
- blocks.push(`
9607
+ suffix.push({ priority: 0, text: `
9562
9608
 
9563
9609
  --- CONSENSUS OUTPUT FORMAT ---
9564
9610
  ${CONSENSUS_OUTPUT_FORMAT}
9565
9611
 
9566
9612
  This section will be used for cross-review with peer agents.
9567
- --- END CONSENSUS OUTPUT FORMAT ---`);
9613
+ --- END CONSENSUS OUTPUT FORMAT ---` });
9568
9614
  } else {
9569
- const hasAnyMeaningfulPart = !!(parts.memory || parts.memoryDir || parts.lens || parts.skills || parts.context || parts.sessionContext || parts.chainContext || parts.specReviewContext || parts.projectStructure || parts.consensusFindings && parts.consensusFindings.length > 0);
9615
+ const hasAnyMeaningfulPart = !!(parts.identity || parts.instructions || parts.memory || parts.memoryDir || parts.lens || parts.skills || parts.context || parts.sessionContext || parts.chainContext || parts.specReviewContext || parts.projectStructure || parts.task || parts.consensusFindings && parts.consensusFindings.length > 0);
9570
9616
  if (hasAnyMeaningfulPart) {
9571
- blocks.push(`
9617
+ suffix.push({ priority: 0, text: `
9572
9618
 
9573
9619
  --- FINDING TAG SCHEMA ---
9574
9620
  ${FINDING_TAG_SCHEMA}
9575
- --- END FINDING TAG SCHEMA ---`);
9621
+ --- END FINDING TAG SCHEMA ---` });
9576
9622
  }
9577
9623
  }
9578
9624
  if (parts.lens) {
9579
- blocks.push(`
9625
+ suffix.push({ priority: 1, text: `
9580
9626
 
9581
9627
  --- LENS ---
9582
9628
  ${parts.lens}
9583
- --- END LENS ---`);
9629
+ --- END LENS ---` });
9584
9630
  }
9585
9631
  if (parts.specReviewContext) {
9586
- blocks.push(`
9632
+ suffix.push({ priority: 2, text: `
9587
9633
 
9588
9634
  --- SPEC REVIEW ---
9589
9635
  ${parts.specReviewContext}
9590
- --- END SPEC REVIEW ---`);
9636
+ --- END SPEC REVIEW ---` });
9591
9637
  }
9592
9638
  if (parts.memory || parts.consensusFindings && parts.consensusFindings.length > 0) {
9593
9639
  const memParts = [];
@@ -9596,14 +9642,14 @@ ${parts.specReviewContext}
9596
9642
  const findingsBlock = "### Recent Consensus Findings\n" + parts.consensusFindings.map((f, i) => `${i + 1}. ${f}`).join("\n");
9597
9643
  memParts.push(findingsBlock);
9598
9644
  }
9599
- blocks.push(`
9645
+ suffix.push({ priority: 3, text: `
9600
9646
 
9601
9647
  --- MEMORY ---
9602
9648
  ${memParts.join("\n\n")}
9603
- --- END MEMORY ---`);
9649
+ --- END MEMORY ---` });
9604
9650
  }
9605
9651
  if (parts.memoryDir) {
9606
- blocks.push(`
9652
+ suffix.push({ priority: 4, text: `
9607
9653
 
9608
9654
  --- AGENT MEMORY ---
9609
9655
  Your persistent memory directory: ${parts.memoryDir}
@@ -9611,27 +9657,69 @@ Save important learnings using file_write to this directory.
9611
9657
  What to save: technology choices, file structure, key patterns, architectural decisions, gotchas.
9612
9658
  Use descriptive filenames like: tech-stack.md, project-structure.md, patterns.md
9613
9659
  Keep entries concise (5-10 lines each). Update existing files rather than creating new ones.
9614
- --- END AGENT MEMORY ---`);
9660
+ --- END AGENT MEMORY ---` });
9615
9661
  }
9616
9662
  if (parts.sessionContext) {
9617
- blocks.push(`
9663
+ suffix.push({ priority: 5, text: `
9618
9664
 
9619
- ${parts.sessionContext}`);
9665
+ ${parts.sessionContext}` });
9620
9666
  }
9621
9667
  if (parts.context) {
9622
- blocks.push(`
9668
+ suffix.push({ priority: 6, text: `
9623
9669
 
9624
9670
  Context:
9625
- ${parts.context}`);
9671
+ ${parts.context}` });
9626
9672
  }
9627
- let assembled = blocks.join("");
9628
- const MAX_PROMPT_CHARS = 3e4;
9629
- if (assembled.length > MAX_PROMPT_CHARS) {
9630
- assembled = assembled.slice(0, MAX_PROMPT_CHARS) + "\n\n[Context truncated to fit budget]";
9631
- }
9632
- return assembled;
9673
+ if (parts.task) {
9674
+ suffix.push({ priority: 0, text: `
9675
+
9676
+ ---
9677
+
9678
+ Task: ${parts.task}` });
9679
+ }
9680
+ const SUFFIX_RESERVE = Math.floor(MAX_ASSEMBLED_PROMPT_CHARS * 0.6);
9681
+ const dropOrderDesc = [...suffix].sort((a, b) => b.priority - a.priority);
9682
+ const dropped = /* @__PURE__ */ new Set();
9683
+ const suffixLen = () => suffix.filter((_, i) => !dropped.has(i)).reduce((acc, seg) => acc + seg.text.length, 0);
9684
+ for (const seg of dropOrderDesc) {
9685
+ if (seg.priority === 0) break;
9686
+ if (suffixLen() <= SUFFIX_RESERVE) break;
9687
+ const idx = suffix.indexOf(seg);
9688
+ if (idx >= 0) dropped.add(idx);
9689
+ }
9690
+ const suffixStr = suffix.filter((_, i) => !dropped.has(i)).map((seg) => seg.text).join("");
9691
+ let prefixStr = prefix.join("");
9692
+ const budget = Math.max(0, MAX_ASSEMBLED_PROMPT_CHARS - suffixStr.length);
9693
+ if (prefixStr.length > budget) {
9694
+ prefixStr = prefixStr.slice(0, budget) + "\n\n[Context truncated to fit budget]";
9695
+ }
9696
+ return prefixStr + suffixStr;
9697
+ }
9698
+ function assembleUtilityPrompt(args) {
9699
+ const { taskId, modelShort, system, user, intro, reentrantCall, relayToken } = args;
9700
+ const relayArgs = relayToken ? `task_id: "${taskId}", relay_token: "${relayToken}", result: "<full agent output>"` : `task_id: "${taskId}", result: "<full agent output>"`;
9701
+ return [
9702
+ {
9703
+ type: "text",
9704
+ text: `${intro}
9705
+
9706
+ \u26A0\uFE0F EXECUTE NOW \u2014 launch this Agent and re-call the tool:
9707
+
9708
+ 1. Agent(model: "${modelShort}", prompt: <AGENT_PROMPT:${taskId} below>, run_in_background: true) \u2014 pass the AGENT_PROMPT:${taskId} content item verbatim
9709
+ 2. When agent completes \u2192 gossip_relay(${relayArgs})
9710
+ 3. Then re-call: ${reentrantCall}
9711
+
9712
+ Do ALL steps in order. Do not wait for user input between them.`
9713
+ },
9714
+ { type: "text", text: `AGENT_PROMPT:${taskId} (_utility)
9715
+ ${system}
9716
+
9717
+ ---
9718
+
9719
+ ${user}` }
9720
+ ];
9633
9721
  }
9634
- var path, DOC_EXTENSIONS, SPEC_PATH_PATTERN, FILE_REF_PATTERN;
9722
+ var path, DOC_EXTENSIONS, SPEC_PATH_PATTERN, FILE_REF_PATTERN, MAX_ASSEMBLED_PROMPT_CHARS;
9635
9723
  var init_prompt_assembler = __esm({
9636
9724
  "packages/orchestrator/src/prompt-assembler.ts"() {
9637
9725
  "use strict";
@@ -9640,6 +9728,7 @@ var init_prompt_assembler = __esm({
9640
9728
  DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".rst"]);
9641
9729
  SPEC_PATH_PATTERN = /(?:docs\/|specs\/|[\w-]+-(?:design|spec)\.md)/;
9642
9730
  FILE_REF_PATTERN = /(?:`([^`]+\.[a-z]{1,6})`|([a-zA-Z][\w/.@-]+\.[a-z]{1,6})(?::\d+)?)/g;
9731
+ MAX_ASSEMBLED_PROMPT_CHARS = 3e4;
9643
9732
  }
9644
9733
  });
9645
9734
 
@@ -12787,6 +12876,20 @@ Return ONLY a JSON array. Use findingId to reference findings:
12787
12876
  [
12788
12877
  { "action": "agree"|"disagree"|"unverified"|"new", "findingId": "agent:f1", "finding": "brief summary", "evidence": "your reasoning", "confidence": 1-5 }
12789
12878
  ]`;
12879
+ let skillsBlock = "";
12880
+ if (this.config.getAgentSkillsContent) {
12881
+ try {
12882
+ const skills = this.config.getAgentSkillsContent(agent.agentId, agent.task);
12883
+ if (skills && skills.trim().length > 0) {
12884
+ skillsBlock = `
12885
+
12886
+ --- SKILLS ---
12887
+ ${skills}
12888
+ --- END SKILLS ---`;
12889
+ }
12890
+ } catch {
12891
+ }
12892
+ }
12790
12893
  const system = `You are a code reviewer performing cross-review. Your job is to verify peer findings against actual code \u2014 catch errors, but also confirm good work.
12791
12894
 
12792
12895
  SOURCE FILES: Always cite original source files, not compiled/bundled build output (dist/, build/, out/). Build artifacts have different line numbers \u2014 citing them causes false verification failures.
@@ -12801,7 +12904,7 @@ VERIFICATION RULES:
12801
12904
  - Do NOT agree with a finding just because it sounds plausible \u2014 verify it
12802
12905
  - Agreeing without verification is WORSE than disagreeing \u2014 a false confirmation poisons the system
12803
12906
 
12804
- Return only valid JSON.`;
12907
+ Return only valid JSON.${skillsBlock}`;
12805
12908
  return { system, user };
12806
12909
  }
12807
12910
  /**
@@ -14244,6 +14347,7 @@ var init_consensus_coordinator = __esm({
14244
14347
  registryGet;
14245
14348
  projectRoot;
14246
14349
  keyProvider;
14350
+ getAgentSkillsContent;
14247
14351
  gossipPublisher = null;
14248
14352
  memWriter;
14249
14353
  currentPhase = "idle";
@@ -14253,6 +14357,7 @@ var init_consensus_coordinator = __esm({
14253
14357
  this.registryGet = config2.registryGet;
14254
14358
  this.projectRoot = config2.projectRoot;
14255
14359
  this.keyProvider = config2.keyProvider;
14360
+ this.getAgentSkillsContent = config2.getAgentSkillsContent;
14256
14361
  this.memWriter = new MemoryWriter(config2.projectRoot);
14257
14362
  }
14258
14363
  setGossipPublisher(publisher) {
@@ -14288,7 +14393,13 @@ var init_consensus_coordinator = __esm({
14288
14393
  }
14289
14394
  agentLlm = (agentId) => agentLlmCache.get(agentId) ?? void 0;
14290
14395
  }
14291
- const engine = new ConsensusEngine({ llm: this.llm, registryGet: this.registryGet, projectRoot: this.projectRoot, agentLlm });
14396
+ const engine = new ConsensusEngine({
14397
+ llm: this.llm,
14398
+ registryGet: this.registryGet,
14399
+ projectRoot: this.projectRoot,
14400
+ agentLlm,
14401
+ getAgentSkillsContent: this.getAgentSkillsContent
14402
+ });
14292
14403
  const consensusReport = await engine.run(results);
14293
14404
  const perfWriter = new PerformanceWriter(this.projectRoot);
14294
14405
  this.currentPhase = "cross_review";
@@ -14587,6 +14698,16 @@ var init_dispatch_pipeline = __esm({
14587
14698
  this.projectStructureCache = parts.length > 0 ? parts.join("\n") : "";
14588
14699
  return this.projectStructureCache || void 0;
14589
14700
  }
14701
+ /**
14702
+ * Invalidate the cached project structure so the next prompt regenerates it.
14703
+ * Call this when the project layout has changed mid-session (e.g. new
14704
+ * top-level packages, agent scaffold, gossip_setup adding agent dirs).
14705
+ * Without this, every prompt sees the boot-time-cached layout and agents
14706
+ * reason against stale structure. Drift audit haiku #8.
14707
+ */
14708
+ invalidateProjectStructureCache() {
14709
+ this.projectStructureCache = null;
14710
+ }
14590
14711
  constructor(config2) {
14591
14712
  this.projectRoot = config2.projectRoot;
14592
14713
  this.workers = config2.workers;
@@ -14607,7 +14728,16 @@ var init_dispatch_pipeline = __esm({
14607
14728
  llm: config2.llm ?? null,
14608
14729
  registryGet: config2.registryGet,
14609
14730
  projectRoot: config2.projectRoot,
14610
- keyProvider: config2.keyProvider ?? null
14731
+ keyProvider: config2.keyProvider ?? null,
14732
+ getAgentSkillsContent: (agentId, task) => {
14733
+ const agentSkills = this.registryGet(agentId)?.skills || [];
14734
+ try {
14735
+ const res = loadSkills(agentId, agentSkills, this.projectRoot, this.skillIndex ?? void 0, task);
14736
+ return res.content || void 0;
14737
+ } catch {
14738
+ return void 0;
14739
+ }
14740
+ }
14611
14741
  });
14612
14742
  try {
14613
14743
  this.catalog = new SkillCatalog(config2.projectRoot);
@@ -17290,6 +17420,9 @@ message: Your question?
17290
17420
  setSkillIndex(index) {
17291
17421
  this.pipeline.setSkillIndex(index);
17292
17422
  }
17423
+ invalidateProjectStructureCache() {
17424
+ this.pipeline.invalidateProjectStructureCache();
17425
+ }
17293
17426
  setSummaryLlm(llm) {
17294
17427
  this.pipeline.setSummaryLlm(llm);
17295
17428
  }
@@ -20554,6 +20687,7 @@ __export(src_exports3, {
20554
20687
  GeminiProvider: () => GeminiProvider,
20555
20688
  GossipPublisher: () => GossipPublisher,
20556
20689
  LensGenerator: () => LensGenerator,
20690
+ MAX_ASSEMBLED_PROMPT_CHARS: () => MAX_ASSEMBLED_PROMPT_CHARS,
20557
20691
  MIN_AGENTS_FOR_CONSENSUS: () => MIN_AGENTS_FOR_CONSENSUS,
20558
20692
  MIN_EVIDENCE: () => MIN_EVIDENCE,
20559
20693
  MainAgent: () => MainAgent,
@@ -20590,10 +20724,12 @@ __export(src_exports3, {
20590
20724
  WorktreeManager: () => WorktreeManager,
20591
20725
  Z_CRITICAL: () => Z_CRITICAL,
20592
20726
  assemblePrompt: () => assemblePrompt,
20727
+ assembleUtilityPrompt: () => assembleUtilityPrompt,
20593
20728
  buildSpecReviewEnrichment: () => buildSpecReviewEnrichment,
20594
20729
  buildToolSystemPrompt: () => buildToolSystemPrompt,
20595
20730
  createHttpBridgeServer: () => createHttpBridgeServer,
20596
20731
  createProvider: () => createProvider,
20732
+ detectFormatCompliance: () => detectFormatCompliance,
20597
20733
  ensureRulesFile: () => ensureRulesFile,
20598
20734
  extractCategories: () => extractCategories,
20599
20735
  extractSpecReferences: () => extractSpecReferences,
@@ -20614,6 +20750,7 @@ var init_src4 = __esm({
20614
20750
  "packages/orchestrator/src/index.ts"() {
20615
20751
  "use strict";
20616
20752
  init_main_agent();
20753
+ init_dispatch_pipeline();
20617
20754
  init_skill_loader();
20618
20755
  init_skill_counters();
20619
20756
  init_worker_agent();
@@ -23500,6 +23637,16 @@ var init_server = __esm({
23500
23637
  connectedAgentIds: this.connectionManager.getAll().map((c) => c.agentId)
23501
23638
  });
23502
23639
  }
23640
+ /**
23641
+ * Update the dashboard's cached agent configs. Call from the MCP server
23642
+ * after gossip_setup writes config.json so the Team page reflects the new
23643
+ * team without requiring /mcp reconnect. The boot-time snapshot
23644
+ * (mcp-server-sdk.ts:365) still wins on initial boot — this method is a
23645
+ * post-setup override, not a replacement.
23646
+ */
23647
+ setAgentConfigs(configs) {
23648
+ this.dashboardRouter?.updateContext({ agentConfigs: configs });
23649
+ }
23503
23650
  };
23504
23651
  }
23505
23652
  });
@@ -38303,6 +38450,36 @@ async function handleNativeRelay(task_id, result, error48, agentStartedAt, relay
38303
38450
  } catch {
38304
38451
  }
38305
38452
  }
38453
+ if (!error48 && !taskInfo.utilityType && agentId !== "_utility") {
38454
+ try {
38455
+ const { PerformanceWriter: PerformanceWriter2, detectFormatCompliance: detectFormatCompliance2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
38456
+ const compliance = detectFormatCompliance2(result ?? "");
38457
+ const metaWriter = new PerformanceWriter2(process.cwd());
38458
+ const now = (/* @__PURE__ */ new Date()).toISOString();
38459
+ metaWriter.appendSignals([
38460
+ { type: "meta", signal: "task_completed", agentId, taskId: task_id, value: elapsed, timestamp: now },
38461
+ {
38462
+ type: "meta",
38463
+ signal: "format_compliance",
38464
+ agentId,
38465
+ taskId: task_id,
38466
+ value: compliance.formatCompliant ? 1 : 0,
38467
+ metadata: {
38468
+ findingCount: compliance.findingCount,
38469
+ citationCount: compliance.citationCount,
38470
+ tags_total: compliance.tags_total,
38471
+ tags_accepted: compliance.tags_accepted,
38472
+ tags_dropped_unknown_type: compliance.tags_dropped_unknown_type,
38473
+ tags_dropped_short_content: compliance.tags_dropped_short_content
38474
+ },
38475
+ timestamp: now
38476
+ }
38477
+ ]);
38478
+ } catch (err) {
38479
+ process.stderr.write(`[gossipcat] native meta signals: ${err.message}
38480
+ `);
38481
+ }
38482
+ }
38306
38483
  if (taskInfo.planId && taskInfo.step && !error48) {
38307
38484
  try {
38308
38485
  ctx.mainAgent.recordPlanStepResult(taskInfo.planId, taskInfo.step, result);
@@ -38666,23 +38843,13 @@ async function handleDispatchSingle(agent_id, task, write_mode, scope, timeout_m
38666
38843
  singleSkillIndex,
38667
38844
  task
38668
38845
  );
38669
- const identityBlock = buildNativeIdentity(agent_id, nativeConfig.model);
38670
- let agentPrompt = [
38671
- identityBlock,
38672
- nativeConfig.instructions || "",
38673
- skillResult.content,
38674
- chainContext ? `
38675
- ${chainContext}
38676
- ` : "",
38677
- `
38678
- ---
38679
-
38680
- Task: ${task}`
38681
- ].filter(Boolean).join("").trim();
38682
- const MAX_AGENT_PROMPT_CHARS = 3e4;
38683
- if (agentPrompt.length > MAX_AGENT_PROMPT_CHARS) {
38684
- agentPrompt = agentPrompt.slice(0, MAX_AGENT_PROMPT_CHARS) + "\n\n[Context truncated to fit budget]";
38685
- }
38846
+ let agentPrompt = assemblePrompt({
38847
+ identity: buildNativeIdentity(agent_id, nativeConfig.model),
38848
+ instructions: nativeConfig.instructions || void 0,
38849
+ skills: skillResult.content || void 0,
38850
+ chainContext: chainContext || void 0,
38851
+ task
38852
+ });
38686
38853
  if (sanitizeResult.sanitized) agentPrompt = prependScopeNote(agentPrompt);
38687
38854
  recordDispatchMetadata(process.cwd(), {
38688
38855
  taskId,
@@ -38834,20 +39001,13 @@ async function handleDispatchParallel(taskDefs, consensus) {
38834
39001
  parallelSkillIndex,
38835
39002
  def.task
38836
39003
  );
38837
- const parallelIdentityBlock = buildNativeIdentity(def.agent_id, nativeConfig.model);
38838
- let agentPrompt = [
38839
- parallelIdentityBlock,
38840
- nativeConfig.instructions || "",
38841
- skillResult.content,
38842
- `
38843
- ---
38844
-
38845
- Task: ${def.task}`
38846
- ].filter(Boolean).join("").trim();
38847
- const MAX_AGENT_PROMPT_CHARS = 3e4;
38848
- if (agentPrompt.length > MAX_AGENT_PROMPT_CHARS) {
38849
- agentPrompt = agentPrompt.slice(0, MAX_AGENT_PROMPT_CHARS) + "\n\n[Context truncated to fit budget]";
38850
- }
39004
+ let agentPrompt = assemblePrompt({
39005
+ identity: buildNativeIdentity(def.agent_id, nativeConfig.model),
39006
+ instructions: nativeConfig.instructions || void 0,
39007
+ skills: skillResult.content || void 0,
39008
+ consensusSummary: consensus || void 0,
39009
+ task: def.task
39010
+ });
38851
39011
  if (def._sandboxSanitized) agentPrompt = prependScopeNote(agentPrompt);
38852
39012
  recordDispatchMetadata(process.cwd(), {
38853
39013
  taskId,
@@ -38965,11 +39125,6 @@ async function handleDispatchConsensus(taskDefs, _utility_task_id) {
38965
39125
  }
38966
39126
  if (errors.length) lines.push(`Relay errors: ${errors.join(", ")}`);
38967
39127
  }
38968
- const consensusInstruction = `
38969
-
38970
- --- CONSENSUS OUTPUT FORMAT ---
38971
- ${CONSENSUS_OUTPUT_FORMAT}
38972
- --- END CONSENSUS OUTPUT FORMAT ---`;
38973
39128
  const nativeInstructions = [];
38974
39129
  const nativePrompts = [];
38975
39130
  for (const def of nativeTasks) {
@@ -38987,11 +39142,7 @@ ${CONSENSUS_OUTPUT_FORMAT}
38987
39142
  process.stderr.write(`[gossipcat] dispatch \u2192 ${def.agent_id} (${nativeConfig.model}) [${taskId}]
38988
39143
  `);
38989
39144
  const rawLens = precomputedLenses?.get(def.agent_id);
38990
- const lensSection = rawLens ? `
38991
-
38992
- --- LENS ---
38993
- ${rawLens.replace(/---\s*(END )?LENS\s*---/gi, "")}
38994
- --- END LENS ---` : "";
39145
+ const lensContent = rawLens ? rawLens.replace(/---\s*(END )?LENS\s*---/gi, "").trim() : void 0;
38995
39146
  let consensusSkillIndex;
38996
39147
  try {
38997
39148
  consensusSkillIndex = ctx.mainAgent.getSkillIndex() ?? void 0;
@@ -39004,23 +39155,14 @@ ${rawLens.replace(/---\s*(END )?LENS\s*---/gi, "")}
39004
39155
  consensusSkillIndex,
39005
39156
  def.task
39006
39157
  );
39007
- const MAX_AGENT_PROMPT_CHARS = 3e4;
39008
- const suffix = consensusInstruction + lensSection + `
39009
-
39010
- ---
39011
-
39012
- Task: ${def.task}`;
39013
- const prefixBudget = Math.max(0, MAX_AGENT_PROMPT_CHARS - suffix.length);
39014
- const consensusIdentityBlock = buildNativeIdentity(def.agent_id, nativeConfig.model);
39015
- let prefix = [
39016
- consensusIdentityBlock,
39017
- nativeConfig.instructions || "",
39018
- skillResultC.content
39019
- ].filter(Boolean).join("").trim();
39020
- if (prefix.length > prefixBudget) {
39021
- prefix = prefix.slice(0, prefixBudget) + "\n\n[Context truncated to fit budget]";
39022
- }
39023
- let agentPrompt = prefix + suffix;
39158
+ let agentPrompt = assemblePrompt({
39159
+ identity: buildNativeIdentity(def.agent_id, nativeConfig.model),
39160
+ instructions: nativeConfig.instructions || void 0,
39161
+ skills: skillResultC.content || void 0,
39162
+ consensusSummary: true,
39163
+ lens: lensContent || void 0,
39164
+ task: def.task
39165
+ });
39024
39166
  lines.push(` ${taskId} \u2192 ${def.agent_id} (native \u2014 dispatch via Agent tool)`);
39025
39167
  nativeInstructions.push(
39026
39168
  `[${taskId}] Agent(model: "${nativeConfig.model}", prompt: <AGENT_PROMPT:${taskId} below>, run_in_background: true)
@@ -39073,6 +39215,15 @@ init_mcp_context();
39073
39215
  init_relay_cross_review();
39074
39216
  init_src3();
39075
39217
  init_src4();
39218
+ var CONSENSUS_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{8}$/;
39219
+ function isValidConsensusId(id) {
39220
+ return typeof id === "string" && CONSENSUS_ID_RE.test(id);
39221
+ }
39222
+ function extractConsensusIdFromFindingId(findingId) {
39223
+ if (typeof findingId !== "string") return void 0;
39224
+ const first = findingId.split(":")[0];
39225
+ return isValidConsensusId(first) ? first : void 0;
39226
+ }
39076
39227
  async function handleCollect(task_ids, timeout_ms, consensus) {
39077
39228
  await ctx.boot();
39078
39229
  if (consensus && (!task_ids || task_ids.length === 0)) {
@@ -39579,10 +39730,11 @@ ${np.user}
39579
39730
  ...consensusReport.unverified || [],
39580
39731
  ...consensusReport.unique || []
39581
39732
  ];
39582
- const provisionalConsensusId = allFindings.length > 0 && typeof allFindings[0].id === "string" ? allFindings[0].id.split(":")[0] : void 0;
39733
+ const authoritativeId = consensusReport?.signals?.[0]?.consensusId;
39734
+ const provisionalConsensusId = (isValidConsensusId(authoritativeId) ? authoritativeId : void 0) ?? (allFindings.length > 0 ? extractConsensusIdFromFindingId(allFindings[0].id) : void 0);
39583
39735
  const provisionalSignals = allFindings.filter((f) => !alreadySignaled.has(f.originalAgentId)).map((f) => ({
39584
39736
  type: "consensus",
39585
- taskId: f.id || "",
39737
+ taskId: provisionalConsensusId || "",
39586
39738
  consensusId: provisionalConsensusId,
39587
39739
  findingId: typeof f.id === "string" ? f.id : void 0,
39588
39740
  signal: tagToSignal[f.tag] || "unique_unconfirmed",
@@ -39958,6 +40110,7 @@ var planExecutionDepth = 0;
39958
40110
  var _pendingSessionData = /* @__PURE__ */ new Map();
39959
40111
  var _pendingSkillData = /* @__PURE__ */ new Map();
39960
40112
  var _pendingVerifyData = /* @__PURE__ */ new Map();
40113
+ var _pendingPlanData = /* @__PURE__ */ new Map();
39961
40114
  var _modules = null;
39962
40115
  function lookupFindingSeverity(findingId, projectRoot) {
39963
40116
  const { existsSync: existsSync44, readdirSync: readdirSync13, readFileSync: readFileSync41 } = require("fs");
@@ -40098,16 +40251,16 @@ async function doBoot() {
40098
40251
  }
40099
40252
  const perfWriter = new m.PerformanceWriter(process.cwd());
40100
40253
  const memorySearcher = new m.MemorySearcher(process.cwd());
40101
- const identityRegistry = /* @__PURE__ */ new Map();
40254
+ ctx.identityRegistry.clear();
40102
40255
  for (const a of agentConfigs) {
40103
- identityRegistry.set(a.id, {
40256
+ ctx.identityRegistry.set(a.id, {
40104
40257
  agent_id: a.id,
40105
40258
  runtime: a.native ? "native" : "relay",
40106
40259
  provider: a.provider,
40107
40260
  model: a.model
40108
40261
  });
40109
40262
  }
40110
- const agentLookup = (id) => identityRegistry.get(id);
40263
+ const agentLookup = (id) => ctx.identityRegistry.get(id);
40111
40264
  ctx.toolServer = new m.ToolServer({
40112
40265
  relayUrl: ctx.relay.url,
40113
40266
  projectRoot: process.cwd(),
@@ -40142,7 +40295,7 @@ async function doBoot() {
40142
40295
  const { join: join51 } = require("path");
40143
40296
  const instructionsPath = join51(process.cwd(), ".gossip", "agents", ac.id, "instructions.md");
40144
40297
  const baseInstructions = existsSync44(instructionsPath) ? readFileSync41(instructionsPath, "utf-8") : "";
40145
- const identity = identityRegistry.get(ac.id);
40298
+ const identity = ctx.identityRegistry.get(ac.id);
40146
40299
  const identityBlock = identity ? m.formatIdentityBlock(identity) + "\n" : "";
40147
40300
  const instructions = (identityBlock + baseInstructions).trim() || void 0;
40148
40301
  const enableWebSearch = ac.preset === "researcher" || (ac.skills ?? []).includes("research");
@@ -40171,10 +40324,13 @@ async function doBoot() {
40171
40324
  agentConfigs.push(ac);
40172
40325
  const modelTier = sa.model.includes("opus") ? "opus" : sa.model.includes("haiku") ? "haiku" : "sonnet";
40173
40326
  ctx.nativeAgentConfigs.set(ac.id, { model: modelTier, instructions: sa.instructions, description: sa.description, skills: ac.skills || [] });
40327
+ ctx.identityRegistry.set(ac.id, { agent_id: ac.id, runtime: "native", provider: ac.provider, model: ac.model });
40174
40328
  process.stderr.write(`[gossipcat] \u{1F916} Registered native agent: ${sa.id} (${modelTier})
40175
40329
  `);
40176
40330
  }
40177
40331
  }
40332
+ ctx.mainProviderConfig = config2.main_agent.provider;
40333
+ ctx.mainModelConfig = config2.main_agent.model;
40178
40334
  let mainProvider = config2.main_agent.provider;
40179
40335
  let mainModel = config2.main_agent.model;
40180
40336
  let mainKey = null;
@@ -40209,6 +40365,7 @@ async function doBoot() {
40209
40365
  }
40210
40366
  }
40211
40367
  ctx.mainProvider = mainProvider;
40368
+ ctx.mainModel = mainModel;
40212
40369
  const supaKey = await ctx.keychain.getKey("supabase");
40213
40370
  const supaTeamSalt = await ctx.keychain.getKey("supabase-team-salt");
40214
40371
  ctx.mainAgent = new m.MainAgent({
@@ -40426,8 +40583,23 @@ async function doSyncWorkers() {
40426
40583
  if (!configPath) return;
40427
40584
  const config2 = m.loadConfig(configPath);
40428
40585
  const agentConfigs = m.configToAgentConfigs(config2);
40586
+ if (config2.main_agent && (config2.main_agent.provider !== ctx.mainProviderConfig || config2.main_agent.model !== ctx.mainModelConfig)) {
40587
+ process.stderr.write(
40588
+ `[gossipcat] \u26A0 main_agent changed in config: ${ctx.mainProviderConfig}/${ctx.mainModelConfig} \u2192 ${config2.main_agent.provider}/${config2.main_agent.model}. Restart Claude Code (/mcp reconnect) for the new orchestrator LLM to take effect.
40589
+ `
40590
+ );
40591
+ ctx.mainProviderConfig = config2.main_agent.provider;
40592
+ ctx.mainModelConfig = config2.main_agent.model;
40593
+ }
40594
+ ctx.identityRegistry.clear();
40429
40595
  for (const ac of agentConfigs) {
40430
40596
  ctx.mainAgent.registerAgent(ac);
40597
+ ctx.identityRegistry.set(ac.id, {
40598
+ agent_id: ac.id,
40599
+ runtime: ac.native ? "native" : "relay",
40600
+ provider: ac.provider,
40601
+ model: ac.model
40602
+ });
40431
40603
  if (ac.native && !ctx.nativeAgentConfigs.has(ac.id)) {
40432
40604
  const { existsSync: ex, readFileSync: rf } = require("fs");
40433
40605
  const { join: j } = require("path");
@@ -40453,6 +40625,7 @@ async function doSyncWorkers() {
40453
40625
  ctx.mainAgent.registerAgent(ac);
40454
40626
  const modelTier = sa.model.includes("opus") ? "opus" : sa.model.includes("haiku") ? "haiku" : "sonnet";
40455
40627
  ctx.nativeAgentConfigs.set(ac.id, { model: modelTier, instructions: sa.instructions, description: sa.description, skills: ac.skills || [] });
40628
+ ctx.identityRegistry.set(ac.id, { agent_id: ac.id, runtime: "native", provider: ac.provider, model: ac.model });
40456
40629
  }
40457
40630
  }
40458
40631
  const added = await ctx.mainAgent.syncWorkers((provider) => ctx.keychain.getKey(provider));
@@ -40467,6 +40640,25 @@ async function doSyncWorkers() {
40467
40640
  process.stderr.write(`[gossipcat] \u{1F504} Synced: ${ctx.workers.size} relay workers + ${ctx.nativeAgentConfigs.size} native agents
40468
40641
  `);
40469
40642
  }
40643
+ try {
40644
+ const skillIndex = ctx.mainAgent.getSkillIndex?.();
40645
+ if (skillIndex) {
40646
+ const merged = [...agentConfigs, ...claudeSubagentsToConfigs2(claudeSubagents)];
40647
+ skillIndex.seedFromConfigs(merged.map((ac) => ({ id: ac.id, skills: ac.skills || [] })));
40648
+ const allIds = merged.map((ac) => ac.id).filter((id) => typeof id === "string" && id.length > 0);
40649
+ if (allIds.length > 0) skillIndex.ensureBoundWithMode(["memory-retrieval"], allIds, "permanent");
40650
+ }
40651
+ } catch {
40652
+ }
40653
+ try {
40654
+ ctx.mainAgent.invalidateProjectStructureCache?.();
40655
+ } catch {
40656
+ }
40657
+ try {
40658
+ const merged = [...agentConfigs, ...claudeSubagentsToConfigs2(claudeSubagents)];
40659
+ ctx.relay?.setAgentConfigs(merged);
40660
+ } catch {
40661
+ }
40470
40662
  } catch (err) {
40471
40663
  process.stderr.write(`[gossipcat] \u274C syncWorkers failed: ${err.message}
40472
40664
  `);
@@ -40484,17 +40676,83 @@ var server = new import_mcp.McpServer(
40484
40676
  instructions: "gossipcat \u2014 multi-agent orchestration. ALWAYS call gossip_status() first when starting work in this project. The gossip_status response loads the orchestrator role, dispatch rules, consensus workflow, native agent relay rule, sandbox enforcement, and other operating rules from .gossip/rules.md. These rules are not in this instruction text \u2014 they live in the gossip_status output to keep the instruction surface small and to allow per-project customization."
40485
40677
  }
40486
40678
  );
40679
+ function buildPlanResponseText(args) {
40680
+ const { task, plan, planned, planId } = args;
40681
+ const taskLines = planned.map((t, i) => {
40682
+ const tag = t.access === "write" ? "[WRITE]" : "[READ]";
40683
+ let line = ` ${i + 1}. ${tag} ${t.agentId || "unassigned"} \u2192 "${t.task}"`;
40684
+ if (t.writeMode) {
40685
+ line += `
40686
+ write_mode: ${t.writeMode}`;
40687
+ if (t.scope) line += ` | scope: ${t.scope}`;
40688
+ }
40689
+ return line;
40690
+ }).join("\n");
40691
+ const assignedTasks = planned.filter((t) => t.agentId);
40692
+ const unassignedTasks = planned.filter((t) => !t.agentId);
40693
+ const planJson = {
40694
+ strategy: plan.strategy,
40695
+ tasks: assignedTasks.map((t, i) => {
40696
+ const entry = { agent_id: t.agentId, task: t.task };
40697
+ if (t.writeMode) entry.write_mode = t.writeMode;
40698
+ if (t.scope) entry.scope = t.scope;
40699
+ entry.plan_id = planId;
40700
+ entry.step = i + 1;
40701
+ return entry;
40702
+ })
40703
+ };
40704
+ let warnings = "";
40705
+ if (plan.warnings?.length) {
40706
+ warnings = `
40707
+ Warnings:
40708
+ ${plan.warnings.map((w) => ` - ${w}`).join("\n")}
40709
+ `;
40710
+ }
40711
+ if (unassignedTasks.length) {
40712
+ warnings += `
40713
+ Unassigned (excluded from PLAN_JSON \u2014 no matching agent):
40714
+ ${unassignedTasks.map((t) => ` - "${t.task}"`).join("\n")}
40715
+ `;
40716
+ }
40717
+ let dispatchBlock;
40718
+ if (plan.strategy === "sequential" || plan.strategy === "single") {
40719
+ const steps = planJson.tasks.map((t, i) => {
40720
+ const args2 = [`agent_id: "${t.agent_id}"`, `task: "${t.task}"`];
40721
+ if (t.write_mode) args2.push(`write_mode: "${t.write_mode}"`);
40722
+ if (t.scope) args2.push(`scope: "${t.scope}"`);
40723
+ args2.push(`plan_id: "${planId}"`, `step: ${i + 1}`);
40724
+ return `Step ${i + 1}: gossip_dispatch(${args2.join(", ")})
40725
+ then: gossip_collect()`;
40726
+ });
40727
+ dispatchBlock = `Execute sequentially:
40728
+ ${steps.join("\n\n")}`;
40729
+ } else {
40730
+ dispatchBlock = `PLAN_JSON (pass to gossip_dispatch with mode:"parallel"):
40731
+ ${JSON.stringify(planJson)}`;
40732
+ }
40733
+ return `Plan: "${task}"
40734
+ Plan ID: ${planId}
40735
+
40736
+ Strategy: ${plan.strategy}
40737
+
40738
+ Tasks:
40739
+ ${taskLines}
40740
+ ${warnings}
40741
+ ---
40742
+ ${dispatchBlock}`;
40743
+ }
40487
40744
  server.tool(
40488
40745
  "gossip_plan",
40489
40746
  'Plan a task with write-mode suggestions. Decomposes into sub-tasks, assigns agents, and classifies each as read or write with suggested write mode. Returns dispatch-ready JSON for approval before execution. Use this before gossip_dispatch(mode:"parallel") for implementation tasks.',
40490
40747
  {
40491
40748
  task: external_exports.string().describe('Task description (e.g. "fix the scope validation bug in packages/tools/")'),
40492
- strategy: external_exports.enum(["parallel", "sequential", "single"]).optional().describe("Override decomposition strategy. Omit to let the orchestrator decide.")
40749
+ strategy: external_exports.enum(["parallel", "sequential", "single"]).optional().describe("Override decomposition strategy. Omit to let the orchestrator decide."),
40750
+ _utility_task_id: external_exports.string().optional().describe("Internal: utility task ID for re-entry after native decomposition")
40493
40751
  },
40494
- async ({ task, strategy }) => {
40752
+ async ({ task, strategy, _utility_task_id }) => {
40495
40753
  await boot();
40496
40754
  await syncWorkersViaKeychain();
40497
- if (planExecutionDepth > 0) {
40755
+ if (planExecutionDepth > 0 && !_utility_task_id) {
40498
40756
  return { content: [{ type: "text", text: "Skipped: already inside a plan step. Execute the task directly instead of re-planning." }] };
40499
40757
  }
40500
40758
  try {
@@ -40507,6 +40765,46 @@ server.tool(
40507
40765
  const registry2 = new AgentRegistry2();
40508
40766
  for (const ac of agentConfigs) registry2.register(ac);
40509
40767
  const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
40768
+ if (_utility_task_id) {
40769
+ const stashed = _pendingPlanData.get(_utility_task_id);
40770
+ if (!stashed) {
40771
+ return { content: [{ type: "text", text: `Plan error: no stashed data for utility task ${_utility_task_id}. Re-run gossip_plan.` }] };
40772
+ }
40773
+ const utilityResult = ctx.nativeResultMap.get(_utility_task_id);
40774
+ _pendingPlanData.delete(_utility_task_id);
40775
+ ctx.nativeResultMap.delete(_utility_task_id);
40776
+ ctx.nativeTaskMap.delete(_utility_task_id);
40777
+ if (!utilityResult || utilityResult.status !== "completed" || !utilityResult.result) {
40778
+ process.stderr.write(`[gossipcat] gossip_plan native utility ${_utility_task_id} failed/timed out
40779
+ `);
40780
+ return { content: [{ type: "text", text: `Plan error: native decomposition failed or timed out. Configure an API key (gossipcat setup) and retry.` }] };
40781
+ }
40782
+ const dispatcher2 = new TaskDispatcher2(null, registry2);
40783
+ const plan2 = dispatcher2.decomposeFromRaw(stashed.task, utilityResult.result);
40784
+ if (stashed.strategy) plan2.strategy = stashed.strategy;
40785
+ dispatcher2.assignAgents(plan2);
40786
+ const planned2 = dispatcher2.classifyWriteModesFallback(plan2);
40787
+ const planId2 = (0, import_crypto20.randomUUID)().slice(0, 8);
40788
+ const assignedTasks2 = planned2.filter((t) => t.agentId);
40789
+ const planState2 = {
40790
+ id: planId2,
40791
+ task: stashed.task,
40792
+ strategy: plan2.strategy,
40793
+ steps: assignedTasks2.map((t, i) => ({
40794
+ step: i + 1,
40795
+ agentId: t.agentId,
40796
+ task: t.task,
40797
+ writeMode: t.writeMode,
40798
+ scope: t.scope
40799
+ })),
40800
+ createdAt: Date.now()
40801
+ };
40802
+ ctx.mainAgent.registerPlan(planState2);
40803
+ const text2 = buildPlanResponseText({ task: stashed.task, plan: plan2, planned: planned2, planId: planId2 });
40804
+ return { content: [{ type: "text", text: `${text2}
40805
+
40806
+ Note: write-mode classification unavailable on this native-only install \u2014 all tasks default to read. Configure a relay API key to enable full classification.` }] };
40807
+ }
40510
40808
  let llm;
40511
40809
  const mainKey = await ctx.keychain.getKey(config2.main_agent.provider);
40512
40810
  if (mainKey) {
@@ -40521,26 +40819,55 @@ server.tool(
40521
40819
  break;
40522
40820
  }
40523
40821
  }
40524
- if (!llm) return { content: [{ type: "text", text: "No API keys available. Run gossipcat setup to configure keys." }] };
40822
+ if (!llm) {
40823
+ if (ctx.nativeUtilityConfig) {
40824
+ const utilityTaskId = (0, import_crypto20.randomUUID)().slice(0, 8);
40825
+ const relayToken = (0, import_crypto20.randomUUID)().slice(0, 12);
40826
+ const dispatcher2 = new TaskDispatcher2(null, registry2);
40827
+ const messages = dispatcher2.buildDecomposeMessages(task);
40828
+ const asString = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((x) => typeof x === "string" ? x : x?.text ?? "").join("") : "";
40829
+ const system = asString(messages.find((m) => m.role === "system")?.content);
40830
+ const user = asString(messages.find((m) => m.role === "user")?.content);
40831
+ _pendingPlanData.set(utilityTaskId, { task, strategy });
40832
+ const UTILITY_TTL_MS = 12e4;
40833
+ ctx.nativeTaskMap.set(utilityTaskId, {
40834
+ agentId: "_utility",
40835
+ task: `plan:${task.slice(0, 120)}`,
40836
+ startedAt: Date.now(),
40837
+ timeoutMs: UTILITY_TTL_MS,
40838
+ utilityType: "plan",
40839
+ relayToken
40840
+ });
40841
+ spawnTimeoutWatcher(utilityTaskId, ctx.nativeTaskMap.get(utilityTaskId));
40842
+ const STASH_TTL_MS = UTILITY_TTL_MS + 3e4;
40843
+ setTimeout(() => {
40844
+ _pendingPlanData.delete(utilityTaskId);
40845
+ }, STASH_TTL_MS).unref();
40846
+ const { assembleUtilityPrompt: assembleUtilityPrompt2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
40847
+ const modelShort = ctx.nativeUtilityConfig.model;
40848
+ return {
40849
+ content: assembleUtilityPrompt2({
40850
+ taskId: utilityTaskId,
40851
+ modelShort,
40852
+ system,
40853
+ user,
40854
+ relayToken,
40855
+ intro: "No API keys available. Dispatching native utility for decomposition.",
40856
+ // F19: JSON.stringify handles backslashes + newlines + quotes correctly.
40857
+ reentrantCall: `gossip_plan(task: ${JSON.stringify(task)}, _utility_task_id: "${utilityTaskId}")`
40858
+ })
40859
+ };
40860
+ }
40861
+ return { content: [{ type: "text", text: "No API keys available. Run gossipcat setup to configure keys." }] };
40862
+ }
40525
40863
  }
40526
40864
  const dispatcher = new TaskDispatcher2(llm, registry2);
40527
40865
  const plan = await dispatcher.decompose(task);
40528
40866
  if (strategy) plan.strategy = strategy;
40529
40867
  dispatcher.assignAgents(plan);
40530
40868
  const planned = await dispatcher.classifyWriteModes(plan);
40531
- const taskLines = planned.map((t, i) => {
40532
- const tag = t.access === "write" ? "[WRITE]" : "[READ]";
40533
- let line = ` ${i + 1}. ${tag} ${t.agentId || "unassigned"} \u2192 "${t.task}"`;
40534
- if (t.writeMode) {
40535
- line += `
40536
- write_mode: ${t.writeMode}`;
40537
- if (t.scope) line += ` | scope: ${t.scope}`;
40538
- }
40539
- return line;
40540
- }).join("\n");
40541
- const assignedTasks = planned.filter((t) => t.agentId);
40542
- const unassignedTasks = planned.filter((t) => !t.agentId);
40543
40869
  const planId = (0, import_crypto20.randomUUID)().slice(0, 8);
40870
+ const assignedTasks = planned.filter((t) => t.agentId);
40544
40871
  const planState = {
40545
40872
  id: planId,
40546
40873
  task,
@@ -40555,56 +40882,7 @@ server.tool(
40555
40882
  createdAt: Date.now()
40556
40883
  };
40557
40884
  ctx.mainAgent.registerPlan(planState);
40558
- const planJson = {
40559
- strategy: plan.strategy,
40560
- tasks: assignedTasks.map((t, i) => {
40561
- const entry = { agent_id: t.agentId, task: t.task };
40562
- if (t.writeMode) entry.write_mode = t.writeMode;
40563
- if (t.scope) entry.scope = t.scope;
40564
- entry.plan_id = planId;
40565
- entry.step = i + 1;
40566
- return entry;
40567
- })
40568
- };
40569
- let warnings = "";
40570
- if (plan.warnings?.length) {
40571
- warnings = `
40572
- Warnings:
40573
- ${plan.warnings.map((w) => ` - ${w}`).join("\n")}
40574
- `;
40575
- }
40576
- if (unassignedTasks.length) {
40577
- warnings += `
40578
- Unassigned (excluded from PLAN_JSON \u2014 no matching agent):
40579
- ${unassignedTasks.map((t) => ` - "${t.task}"`).join("\n")}
40580
- `;
40581
- }
40582
- let dispatchBlock;
40583
- if (plan.strategy === "sequential" || plan.strategy === "single") {
40584
- const steps = planJson.tasks.map((t, i) => {
40585
- const args = [`agent_id: "${t.agent_id}"`, `task: "${t.task}"`];
40586
- if (t.write_mode) args.push(`write_mode: "${t.write_mode}"`);
40587
- if (t.scope) args.push(`scope: "${t.scope}"`);
40588
- args.push(`plan_id: "${planId}"`, `step: ${i + 1}`);
40589
- return `Step ${i + 1}: gossip_dispatch(${args.join(", ")})
40590
- then: gossip_collect()`;
40591
- });
40592
- dispatchBlock = `Execute sequentially:
40593
- ${steps.join("\n\n")}`;
40594
- } else {
40595
- dispatchBlock = `PLAN_JSON (pass to gossip_dispatch with mode:"parallel"):
40596
- ${JSON.stringify(planJson)}`;
40597
- }
40598
- const text = `Plan: "${task}"
40599
- Plan ID: ${planId}
40600
-
40601
- Strategy: ${plan.strategy}
40602
-
40603
- Tasks:
40604
- ${taskLines}
40605
- ${warnings}
40606
- ---
40607
- ${dispatchBlock}`;
40885
+ const text = buildPlanResponseText({ task, plan, planned, planId });
40608
40886
  return { content: [{ type: "text", text }] };
40609
40887
  } catch (err) {
40610
40888
  return { content: [{ type: "text", text: `Plan error: ${err.message}` }] };
@@ -41155,6 +41433,12 @@ server.tool(
41155
41433
  }
41156
41434
  mkdirSync22(join51(root, ".gossip"), { recursive: true });
41157
41435
  writeFileSync18(join51(root, ".gossip", "config.json"), JSON.stringify(config2, null, 2));
41436
+ try {
41437
+ await syncWorkersViaKeychain();
41438
+ } catch (e) {
41439
+ process.stderr.write(`[gossipcat] gossip_setup: failed to refresh agent state: ${e}
41440
+ `);
41441
+ }
41158
41442
  const agentList = Object.entries(config2.agents).map(([id, a]) => `- ${id}: ${a.provider}/${a.model} (${a.preset || "custom"})${a.native ? " \u2014 native" : ""}`).join("\n");
41159
41443
  const rulesDir = join51(root, env.rulesDir);
41160
41444
  const rulesFile = join51(root, env.rulesFile);
@@ -41266,102 +41550,12 @@ Then review the plan and dispatch with gossip_dispatch(mode: "parallel", tasks:
41266
41550
  }
41267
41551
  }
41268
41552
  const isNative = ctx.nativeAgentConfigs.has(agent_id);
41269
- let _runSanitized = false;
41270
- try {
41271
- const { relativizeProjectPaths: relativizeProjectPaths2, readSandboxMode: readSandboxMode2, shouldSanitize: shouldSanitize2 } = (init_sandbox2(), __toCommonJS(sandbox_exports));
41272
- if (readSandboxMode2(process.cwd()) !== "off") {
41273
- const preset = (() => {
41274
- try {
41275
- return ctx.mainAgent.getAgentList?.().find((a) => a.id === agent_id)?.preset;
41276
- } catch {
41277
- return void 0;
41278
- }
41279
- })();
41280
- if (shouldSanitize2(write_mode, preset)) {
41281
- const { sanitized, replacements } = relativizeProjectPaths2(task, process.cwd());
41282
- if (replacements > 0) {
41283
- process.stderr.write(`[gossipcat] \u{1F9F9} sanitized ${replacements} project path(s) in task for ${agent_id}
41284
- `);
41285
- }
41286
- task = sanitized;
41287
- _runSanitized = true;
41288
- }
41289
- }
41290
- } catch {
41553
+ if (isNative) {
41554
+ return handleDispatchSingle(agent_id, task, write_mode, scope);
41291
41555
  }
41292
41556
  const options = {};
41293
41557
  if (write_mode) options.writeMode = write_mode;
41294
41558
  if (scope) options.scope = scope;
41295
- if (isNative) {
41296
- if (write_mode === "scoped") {
41297
- if (!scope) {
41298
- return { content: [{ type: "text", text: "Error: scoped write mode requires a scope path" }] };
41299
- }
41300
- const overlap = ctx.mainAgent.scopeTracker.hasOverlap(scope);
41301
- if (overlap.overlaps) {
41302
- return { content: [{ type: "text", text: `Error: Scope "${scope}" conflicts with running task ${overlap.conflictTaskId} at "${overlap.conflictScope}"` }] };
41303
- }
41304
- }
41305
- evictStaleNativeTasks();
41306
- const taskId = require("crypto").randomUUID().slice(0, 8);
41307
- const relayToken = require("crypto").randomUUID().slice(0, 12);
41308
- ctx.nativeTaskMap.set(taskId, { agentId: agent_id, task, startedAt: Date.now(), timeoutMs: NATIVE_TASK_TTL_MS, relayToken });
41309
- spawnTimeoutWatcher(taskId, ctx.nativeTaskMap.get(taskId));
41310
- persistNativeTaskMap();
41311
- try {
41312
- ctx.mainAgent.recordNativeTask(taskId, agent_id, task);
41313
- } catch {
41314
- }
41315
- if (write_mode === "scoped" && scope) {
41316
- ctx.mainAgent.scopeTracker.register(scope, taskId);
41317
- }
41318
- const config2 = ctx.nativeAgentConfigs.get(agent_id);
41319
- const basePrompt = config2.instructions || `You are a skilled ${config2.description || "agent"}. Complete the task thoroughly.`;
41320
- const scopePrefix = write_mode === "scoped" && scope ? `SCOPE RESTRICTION: Only modify files within ${scope}. Do not edit files outside this directory.
41321
-
41322
- ` : "";
41323
- let agentPrompt = `${scopePrefix}${basePrompt}
41324
-
41325
- ---
41326
-
41327
- Task: ${task}`;
41328
- if (_runSanitized) {
41329
- try {
41330
- const { prependScopeNote: prependScopeNote2 } = (init_sandbox2(), __toCommonJS(sandbox_exports));
41331
- agentPrompt = prependScopeNote2(agentPrompt);
41332
- } catch {
41333
- }
41334
- }
41335
- try {
41336
- const { recordDispatchMetadata: recordDispatchMetadata2 } = (init_sandbox2(), __toCommonJS(sandbox_exports));
41337
- recordDispatchMetadata2(process.cwd(), {
41338
- taskId,
41339
- agentId: agent_id,
41340
- writeMode: write_mode,
41341
- scope,
41342
- timestamp: Date.now()
41343
- });
41344
- } catch {
41345
- }
41346
- const modelShort = config2.model || "sonnet";
41347
- return {
41348
- content: [
41349
- {
41350
- type: "text",
41351
- text: `Dispatched to ${agent_id} (native). Task ID: ${taskId}
41352
-
41353
- \u26A0\uFE0F EXECUTE NOW \u2014 launch this Agent and relay the result:
41354
-
41355
- 1. Agent(model: "${modelShort}", prompt: <AGENT_PROMPT:${taskId} below>, run_in_background: true) \u2014 pass the AGENT_PROMPT:${taskId} content item verbatim
41356
- 2. When agent completes \u2192 gossip_relay(task_id: "${taskId}", relay_token: "${relayToken}", result: "<full agent output>")
41357
-
41358
- Do BOTH steps in your next response. Do not wait for user input between them.`
41359
- },
41360
- { type: "text", text: `AGENT_PROMPT:${taskId} (${agent_id})
41361
- ${agentPrompt}` }
41362
- ]
41363
- };
41364
- }
41365
41559
  if (!ctx.workers.has(agent_id)) {
41366
41560
  await syncWorkersViaKeychain();
41367
41561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gossipcat",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Multi-agent orchestration for Claude Code — parallel review, consensus, adaptive dispatch",
5
5
  "mcpName": "io.github.ataberk-xyz/gossipcat",
6
6
  "repository": {