gossipcat 0.4.26 → 0.4.28

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.
@@ -10033,7 +10033,7 @@ function coerceStatus(raw) {
10033
10033
  }
10034
10034
  return "pending";
10035
10035
  }
10036
- var import_fs9, import_crypto8, import_path10, SAFE_NAME, __SKILL_ENGINE_TEST_HOOK, VALID_STATUSES, KNOWN_CATEGORIES, CATEGORY_KEYWORDS, REQUIRED_SECTIONS, BUNDLED_TEMPLATE, SkillEngine;
10036
+ var import_fs9, import_crypto8, import_path10, SAFE_NAME, __SKILL_ENGINE_TEST_HOOK, VALID_STATUSES, TECH_STACK_MIN_DEPS, MANIFEST_HINTS, KNOWN_CATEGORIES, CATEGORY_KEYWORDS, REQUIRED_SECTIONS, BUNDLED_TEMPLATE, SkillEngine;
10037
10037
  var init_skill_engine = __esm({
10038
10038
  "packages/orchestrator/src/skill-engine.ts"() {
10039
10039
  "use strict";
@@ -10056,6 +10056,17 @@ var init_skill_engine = __esm({
10056
10056
  "silent_skill",
10057
10057
  "insufficient_evidence"
10058
10058
  ];
10059
+ TECH_STACK_MIN_DEPS = 3;
10060
+ MANIFEST_HINTS = [
10061
+ ["Cargo.toml", "Rust"],
10062
+ ["pyproject.toml", "Python"],
10063
+ ["requirements.txt", "Python"],
10064
+ ["go.mod", "Go"],
10065
+ ["foundry.toml", "Solidity/Foundry"],
10066
+ ["Move.toml", "Move/Aptos/Sui"],
10067
+ ["Gemfile", "Ruby"],
10068
+ ["composer.json", "PHP"]
10069
+ ];
10059
10070
  KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
10060
10071
  "trust_boundaries",
10061
10072
  "injection_vectors",
@@ -10356,7 +10367,7 @@ NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.
10356
10367
  projectContext = (0, import_fs9.readFileSync)(bootstrapPath, "utf-8").slice(0, 1500);
10357
10368
  }
10358
10369
  if (this.techStackCache === void 0) {
10359
- this.techStackCache = await this.detectTechStack();
10370
+ this.techStackCache = this.readTechStackOverride() ?? await this.detectTechStack();
10360
10371
  }
10361
10372
  const techStack = this.techStackCache;
10362
10373
  if (techStack) {
@@ -10531,6 +10542,7 @@ ${fm}
10531
10542
  */
10532
10543
  async detectTechStack() {
10533
10544
  const inputs = [];
10545
+ let totalDepCount = 0;
10534
10546
  const pkgPaths = [(0, import_path10.join)(this.projectRoot, "package.json")];
10535
10547
  try {
10536
10548
  const packagesDir = (0, import_path10.join)(this.projectRoot, "packages");
@@ -10546,6 +10558,7 @@ ${fm}
10546
10558
  try {
10547
10559
  const pkg = JSON.parse((0, import_fs9.readFileSync)(p, "utf-8"));
10548
10560
  const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
10561
+ totalDepCount += deps.length;
10549
10562
  if (deps.length > 0) {
10550
10563
  inputs.push(`${p.replace(this.projectRoot + "/", "")}: ${deps.join(", ")}`);
10551
10564
  }
@@ -10557,7 +10570,59 @@ ${fm}
10557
10570
  inputs.push(`Source dirs: ${srcDirs.join(", ") || "root"}`);
10558
10571
  } catch {
10559
10572
  }
10560
- if (inputs.length === 0) return null;
10573
+ let manifestCount = 0;
10574
+ for (const [filename, language] of MANIFEST_HINTS) {
10575
+ if ((0, import_fs9.existsSync)((0, import_path10.join)(this.projectRoot, filename))) {
10576
+ inputs.push(`Manifest: ${filename} (${language})`);
10577
+ manifestCount++;
10578
+ }
10579
+ }
10580
+ const READMES = ["README.md", "README", "readme.md"];
10581
+ let readmeFound = false;
10582
+ for (const name of READMES) {
10583
+ const p = (0, import_path10.join)(this.projectRoot, name);
10584
+ if ((0, import_fs9.existsSync)(p)) {
10585
+ try {
10586
+ const content = (0, import_fs9.readFileSync)(p, "utf-8");
10587
+ const lines = content.split("\n").slice(0, 30).join("\n");
10588
+ const clamped = lines.slice(0, 2e3);
10589
+ if (clamped.trim()) {
10590
+ inputs.push(`README (${name}, first ${Math.min(30, content.split("\n").length)} lines):
10591
+ ${clamped}`);
10592
+ readmeFound = true;
10593
+ break;
10594
+ }
10595
+ } catch {
10596
+ }
10597
+ }
10598
+ }
10599
+ const EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".gossip", "dist", "build", "out", "coverage"]);
10600
+ const EXCLUDED_EXTS = /* @__PURE__ */ new Set([".json", ".md", ".yaml", ".yml", ".toml", ".lock", ".txt", ".xml", ".ini", ".cfg", ".env", ".gitignore", ".gitattributes", ".editorconfig", ".npmrc", ".nvmrc", ".prettierrc", ".eslintrc", ".babelrc", ".dockerignore", ".flowconfig"]);
10601
+ let extensionSignal = false;
10602
+ try {
10603
+ const entries = (0, import_fs9.readdirSync)(this.projectRoot, { withFileTypes: true });
10604
+ const extCounts = /* @__PURE__ */ new Map();
10605
+ for (const entry of entries) {
10606
+ if (EXCLUDED_DIRS.has(entry.name)) continue;
10607
+ if (entry.isFile()) {
10608
+ const dotIdx = entry.name.lastIndexOf(".");
10609
+ if (dotIdx > 0) {
10610
+ const ext = entry.name.slice(dotIdx).toLowerCase();
10611
+ if (!EXCLUDED_EXTS.has(ext)) {
10612
+ extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
10613
+ }
10614
+ }
10615
+ }
10616
+ }
10617
+ if (extCounts.size > 0) {
10618
+ const sorted = Array.from(extCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([ext, count]) => `${ext}(${count})`).join(", ");
10619
+ inputs.push(`Root file extensions: ${sorted}`);
10620
+ extensionSignal = true;
10621
+ }
10622
+ } catch {
10623
+ }
10624
+ const hasNonNodeSignal = manifestCount > 0 || readmeFound || extensionSignal;
10625
+ if (totalDepCount < TECH_STACK_MIN_DEPS && !hasNonNodeSignal) return null;
10561
10626
  try {
10562
10627
  const messages = [{
10563
10628
  role: "user",
@@ -10579,6 +10644,35 @@ ${inputs.join("\n")}
10579
10644
  return inputs.join("\n").slice(0, 500);
10580
10645
  }
10581
10646
  }
10647
+ /**
10648
+ * Reads `.gossip/tech-stack.md` as a user-authored override for tech-stack
10649
+ * detection. Returns the file content (clamped to 2000 chars) if present and
10650
+ * non-empty, or null to fall through to auto-detect.
10651
+ *
10652
+ * Operator escape hatch for non-Node host projects (Solidity, Rust, Move, etc.)
10653
+ * where the LLM hallucinates a Node.js stack from thin npm dep signal.
10654
+ * Cache is session-stable via techStackCache — restart the MCP server to pick
10655
+ * up edits to this file.
10656
+ */
10657
+ readTechStackOverride() {
10658
+ const overridePath = (0, import_path10.join)(this.projectRoot, ".gossip", "tech-stack.md");
10659
+ if (!(0, import_fs9.existsSync)(overridePath)) return null;
10660
+ try {
10661
+ const { size } = (0, import_fs9.statSync)(overridePath);
10662
+ if (size > 2048) {
10663
+ process.stderr.write(`[skill-engine] tech-stack.md override is ${size} bytes, clamping to 2000 chars
10664
+ `);
10665
+ }
10666
+ const content = (0, import_fs9.readFileSync)(overridePath, "utf-8").slice(0, 2e3).trim();
10667
+ if (!content) return null;
10668
+ return content;
10669
+ } catch (err) {
10670
+ const msg = err instanceof Error ? err.message : String(err);
10671
+ process.stderr.write(`[skill-engine] tech-stack.md override read failed: ${msg}
10672
+ `);
10673
+ return null;
10674
+ }
10675
+ }
10582
10676
  loadCategoryFindings(category) {
10583
10677
  const filePath = (0, import_path10.join)(this.projectRoot, ".gossip", "agent-performance.jsonl");
10584
10678
  if (!(0, import_fs9.existsSync)(filePath)) return [];
@@ -16468,7 +16562,7 @@ var init_parse_findings = __esm({
16468
16562
  TYPE_ATTR_PATTERN = /type="([a-zA-Z]+)"/;
16469
16563
  SEVERITY_ATTR_PATTERN = /severity="(critical|high|medium|low)"/;
16470
16564
  CATEGORY_ATTR_PATTERN = /category="([a-z_]+)"/;
16471
- ANCHOR_PATTERN = /[\w./-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|md|json|yaml|yml|toml|sh):\d+/;
16565
+ ANCHOR_PATTERN = /(?:[a-zA-Z]:\/)?[\w./-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|md|json|yaml|yml|toml|sh):\d+/;
16472
16566
  CANONICAL_TYPES = /* @__PURE__ */ new Set(["finding", "suggestion", "insight"]);
16473
16567
  HTML_ENTITY_OPEN_PATTERN = /&lt;agent_finding\b/gi;
16474
16568
  HTML_ENTITY_CLOSE_PATTERN = /&lt;\/agent_finding&gt;/gi;
@@ -16642,7 +16736,7 @@ var init_consensus_engine = __esm({
16642
16736
  };
16643
16737
  MIN_FINDINGS_PER_PEER = 2;
16644
16738
  VALID_ACTIONS = /* @__PURE__ */ new Set(["agree", "disagree", "unverified", "new"]);
16645
- ANCHOR_PATTERN2 = /[\w./-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|md|json|yaml|yml|toml|sh):\d+/;
16739
+ ANCHOR_PATTERN2 = /(?:[a-zA-Z]:\/)?[\w./-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|md|json|yaml|yml|toml|sh):\d+/;
16646
16740
  MAX_VERIFIER_TURNS = 7;
16647
16741
  VERIFIER_TOOLS = [
16648
16742
  { name: "file_read", description: "Read file contents", parameters: { type: "object", properties: { path: { type: "string", description: "Absolute or project-relative file path" }, startLine: { type: "number", description: "First line to read (1-based)" }, endLine: { type: "number", description: "Last line to read (inclusive)" } }, required: ["path"] } },
@@ -17668,7 +17762,7 @@ Return only valid JSON.${skillsBlock}`;
17668
17762
  */
17669
17763
  async snippetsForFinding(findingText, maxSnippets = 3) {
17670
17764
  if (!this.config.projectRoot && this.currentWorktreeRoots.size === 0) return "";
17671
- const citationPattern = /((?:[\w./-]+\/)?([a-zA-Z][\w.-]+\.[a-z]{1,6})):(\d+)/g;
17765
+ const citationPattern = /((?:[a-zA-Z]:\/)?(?:[\w./-]+\/)?([a-zA-Z][\w.-]+\.[a-z]{1,6})):(\d+)/g;
17672
17766
  const CONTEXT_LINES = 2;
17673
17767
  const MAX_FILE_SIZE2 = 10 * 1024 * 1024;
17674
17768
  const anchors = [];
@@ -17689,7 +17783,7 @@ Return only valid JSON.${skillsBlock}`;
17689
17783
  anchors.push(`\u26A0 Agent cited \`${safeRef}:${lineNum}\` but file not found`);
17690
17784
  continue;
17691
17785
  }
17692
- const resolvedFromProjectRoot = this.currentWorktreeRoots.size > 0 && this.config.projectRoot && filePath.startsWith((0, import_path28.resolve)(this.config.projectRoot) + "/");
17786
+ const resolvedFromProjectRoot = this.isResolvedFromProjectRootOnly(filePath);
17693
17787
  const fileStat = await (0, import_promises3.stat)(filePath);
17694
17788
  if (fileStat.size > MAX_FILE_SIZE2) continue;
17695
17789
  const content = await this.cachedRead(filePath);
@@ -17723,7 +17817,7 @@ ${safeSnippet}
17723
17817
  const trimmed = value.trim();
17724
17818
  if (!trimmed || trimmed.length > 80) continue;
17725
17819
  if (tag === "file") {
17726
- const fileMatch = trimmed.match(/^((?:[\w./-]+\/)?([a-zA-Z][\w.-]+\.[a-z]{1,6})):(\d+)$/);
17820
+ const fileMatch = trimmed.match(/^((?:[a-zA-Z]:\/)?(?:[\w./-]+\/)?([a-zA-Z][\w.-]+\.[a-z]{1,6})):(\d+)$/);
17727
17821
  if (fileMatch && !seen.has(trimmed)) {
17728
17822
  seen.add(trimmed);
17729
17823
  const fullRef = fileMatch[1];
@@ -17733,7 +17827,7 @@ ${safeSnippet}
17733
17827
  const safeRef = fullRef.replace(/["<>]/g, "");
17734
17828
  const filePath = await this.cachedResolveForAnchor(fullRef) ?? await this.cachedResolveForAnchor(bareFile);
17735
17829
  if (filePath) {
17736
- const resolvedFromProjectRoot = this.currentWorktreeRoots.size > 0 && this.config.projectRoot && filePath.startsWith((0, import_path28.resolve)(this.config.projectRoot) + "/");
17830
+ const resolvedFromProjectRoot = this.isResolvedFromProjectRootOnly(filePath);
17737
17831
  const content = await this.cachedRead(filePath);
17738
17832
  if (content) {
17739
17833
  const fileLines = content.split("\n");
@@ -17813,7 +17907,7 @@ ${safeSnippet}
17813
17907
  return false;
17814
17908
  }
17815
17909
  const stripped = evidence.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "").replace(/<example>[\s\S]*?<\/example>/gi, "").replace(/"[^"\n]*"/g, "").replace(/'[^'\n]*'/g, "");
17816
- const citationPattern = /(?:[\w./-]+\/)?([a-zA-Z][\w.-]+\.[a-z]{1,6}):(\d+)/g;
17910
+ const citationPattern = /(?:(?:[a-zA-Z]:\/)?(?:[\w./-]+\/)?)?([a-zA-Z][\w.-]+\.[a-z]{1,6}):(\d+)/g;
17817
17911
  const rawCitations = [];
17818
17912
  let match;
17819
17913
  while ((match = citationPattern.exec(stripped)) !== null) {
@@ -17859,6 +17953,28 @@ ${safeSnippet}
17859
17953
  * worktree can still be auto-anchored when consensus runs back in the main
17860
17954
  * MCP process).
17861
17955
  */
17956
+ /**
17957
+ * Returns true when filePath falls under projectRoot but NOT inside any
17958
+ * active worktree root. This is the condition that warrants the
17959
+ * "⚠ resolved against project root, NOT worktree" attribute — the file
17960
+ * was found via the projectRoot fallback, meaning it reflects master HEAD,
17961
+ * not the branch under review.
17962
+ *
17963
+ * The previous inline check used only a startsWith(projectRoot) test, which
17964
+ * produced a false-positive when the worktree itself is nested under
17965
+ * projectRoot (the standard `.claude/worktrees/agent-X` layout): a worktree
17966
+ * file like /x/.claude/worktrees/agent-Y/foo.ts passes startsWith(/x/)
17967
+ * even though it was correctly resolved via the worktree priority root.
17968
+ *
17969
+ * Fix: additionally verify the path is NOT inside any currentWorktreeRoot
17970
+ * via isInsideAnyRoot, which applies realpath-safe containment checks.
17971
+ */
17972
+ isResolvedFromProjectRootOnly(filePath) {
17973
+ if (this.currentWorktreeRoots.size === 0) return false;
17974
+ if (!this.config.projectRoot) return false;
17975
+ if (!filePath.startsWith((0, import_path28.resolve)(this.config.projectRoot) + "/")) return false;
17976
+ return !this.isInsideAnyRoot(filePath, [...this.currentWorktreeRoots]);
17977
+ }
17862
17978
  /**
17863
17979
  * Guard: resolved path must stay inside one of the valid roots, with
17864
17980
  * symlink-safe containment.
@@ -19063,7 +19179,7 @@ function detectFormatCompliance(result) {
19063
19179
  const tags_dropped_unknown_type = Object.values(parseRes.droppedUnknownType).reduce((a, b) => a + b, 0) + parseRes.droppedMissingType;
19064
19180
  const tags_dropped_short_content = parseRes.droppedShortContent;
19065
19181
  const findingCount = (body.match(/<agent_finding[\s>]/g) ?? []).length;
19066
- const citationCount = (body.match(/\b[\w./-]+\.\w+:\d+\b/g) ?? []).length;
19182
+ const citationCount = (body.match(/\b(?:[a-zA-Z]:\/)?[\w./-]+\.\w+:\d+\b/g) ?? []).length;
19067
19183
  const formatCompliant = tags_accepted > 0 && citationCount >= tags_accepted;
19068
19184
  const diagnostics = [...parseRes.diagnostics];
19069
19185
  if (truncated) {
@@ -23313,7 +23429,7 @@ var init_dedupe_key = __esm({
23313
23429
  "packages/orchestrator/src/dedupe-key.ts"() {
23314
23430
  "use strict";
23315
23431
  import_crypto15 = require("crypto");
23316
- ANCHOR_PATTERN3 = /[\w./-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|md|json|yaml|yml|toml|sh):\d+/;
23432
+ ANCHOR_PATTERN3 = /(?:[a-zA-Z]:\/)?[\w./-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|md|json|yaml|yml|toml|sh):\d+/;
23317
23433
  MIN_NORMALIZED_CONTENT_LENGTH = 32;
23318
23434
  DEDUPE_KEY_INTERNALS = {
23319
23435
  MIN_NORMALIZED_CONTENT_LENGTH,
@@ -31022,11 +31138,11 @@ var init_routes = __esm({
31022
31138
  }
31023
31139
  if (!existsSync68(reportsDir)) return { reports: [], totalReports: 0, page, pageSize, retractedConsensusIds, roundRetractions };
31024
31140
  try {
31025
- const { statSync: statSync32 } = require("fs");
31141
+ const { statSync: statSync33 } = require("fs");
31026
31142
  const allFiles = readdirSync22(reportsDir).filter((f) => f.endsWith(".json")).sort((a, b) => {
31027
31143
  try {
31028
- const aTime = statSync32((0, import_path68.join)(reportsDir, a)).mtimeMs;
31029
- const bTime = statSync32((0, import_path68.join)(reportsDir, b)).mtimeMs;
31144
+ const aTime = statSync33((0, import_path68.join)(reportsDir, a)).mtimeMs;
31145
+ const bTime = statSync33((0, import_path68.join)(reportsDir, b)).mtimeMs;
31030
31146
  return bTime - aTime;
31031
31147
  } catch {
31032
31148
  return 0;
@@ -49337,7 +49453,10 @@ async function resolveDispatchResolutionRoots(dispatchResolutionRoots) {
49337
49453
  );
49338
49454
  }
49339
49455
  if (discovered.length > 0) {
49340
- effectiveRoots = discovered;
49456
+ const hashedPaths = discovered.map((d) => hashPath(d));
49457
+ warnings.push(
49458
+ `autoDiscoverWorktrees: ${discovered.length} sibling worktree(s) discovered but auto-promotion is disabled. Pass resolutionRoots to gossip_dispatch to pin cross-reviewers to a specific worktree. Discovered (hashed): ${hashedPaths.join(", ")}`
49459
+ );
49341
49460
  } else if (rejected.length > 0) {
49342
49461
  warnings.push(
49343
49462
  `autoDiscoverWorktrees: ${rejected.length} candidate(s) failed validation; cross-review will use projectRoot only. See stderr for rejection reasons.`
@@ -50046,10 +50165,10 @@ ${t.skillWarnings.map((w) => ` - ${w}`).join("\n")}`;
50046
50165
  if (!mainLlm) {
50047
50166
  return { content: [{ type: "text", text: "Error: No LLM configured for consensus. Check gossip_setup." }] };
50048
50167
  }
50049
- const { PerformanceReader: PerformanceReader3, discoverGitWorktrees: discoverGitWorktrees2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
50168
+ const { PerformanceReader: PerformanceReader3, discoverGitWorktrees: discoverGitWorktrees2, hashPath: hashPath2 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
50050
50169
  const performanceReader = new PerformanceReader3(process.cwd());
50051
50170
  const explicitRoots = resolutionRoots ?? [];
50052
- let effectiveRoots = explicitRoots;
50171
+ const effectiveRoots = explicitRoots;
50053
50172
  try {
50054
50173
  const { findConfigPath: findConfigPath2, loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
50055
50174
  const cfgPath = findConfigPath2(process.cwd());
@@ -50062,7 +50181,13 @@ ${t.skillWarnings.map((w) => ` - ${w}`).join("\n")}`;
50062
50181
  `
50063
50182
  );
50064
50183
  }
50065
- effectiveRoots = [...explicitRoots, ...discovered];
50184
+ if (discovered.length > 0) {
50185
+ const hashedPaths = discovered.map((d) => hashPath2(d));
50186
+ process.stderr.write(
50187
+ `[consensus] autoDiscoverWorktrees: ${discovered.length} sibling worktree(s) discovered but auto-promotion is disabled (issue #402). Pass resolutionRoots to gossip_collect to pin cross-reviewers. Discovered (hashed): ${hashedPaths.join(", ")}
50188
+ `
50189
+ );
50190
+ }
50066
50191
  }
50067
50192
  } catch (err) {
50068
50193
  process.stderr.write(`[consensus] auto-discovery failed: ${err.message}
@@ -52109,7 +52234,7 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
52109
52234
  } catch {
52110
52235
  }
52111
52236
  try {
52112
- const { readdirSync: readdirSync22, statSync: statSync32 } = await import("fs");
52237
+ const { readdirSync: readdirSync22, statSync: statSync33 } = await import("fs");
52113
52238
  const { readJsonlWithRotated: readJsonlRotated1506 } = await Promise.resolve().then(() => (init_src4(), src_exports3));
52114
52239
  const reportsDir = (0, import_path84.join)(process.cwd(), ".gossip", "consensus-reports");
52115
52240
  const perfPath = (0, import_path84.join)(process.cwd(), ".gossip", "agent-performance.jsonl");
@@ -52120,7 +52245,7 @@ Note: write-mode classification unavailable on this native-only install \u2014 a
52120
52245
  for (const fname of readdirSync22(reportsDir)) {
52121
52246
  if (!fname.endsWith(".json")) continue;
52122
52247
  const fpath = (0, import_path84.join)(reportsDir, fname);
52123
- const st = statSync32(fpath);
52248
+ const st = statSync33(fpath);
52124
52249
  if (now - st.mtimeMs > WINDOW_MS2) continue;
52125
52250
  recentReports.push({ id: fname.replace(/\.json$/, ""), mtimeMs: st.mtimeMs });
52126
52251
  }
package/docs/HANDBOOK.md CHANGED
@@ -228,6 +228,12 @@ If `format_compliance` signals a sudden drop for an agent that was fine last rou
228
228
 
229
229
  When blocked, the error message shows age + remaining cooldown + override instruction. Pass `force: true` to bypass; every override is appended to `.gossip/forced-skill-develops.jsonl` for auditability. Chronic override patterns on an agent+category pair are a signal that the skill prompt is ineffective or `MIN_EVIDENCE` is miscalibrated for that category — investigate before reflexively forcing.
230
230
 
231
+ ### Tech-stack override
232
+
233
+ Drop a `.gossip/tech-stack.md` at the project root to bypass auto-detection on `gossip_skills(action: "develop")`. Content (max 2000 chars after trim) is injected verbatim into the skill-develop prompt's `<tech_stack>` block, replacing the auto-detected description. Useful for non-Node host projects (Solidity, Rust, Move, audit workspaces) where the LLM hallucinates a Node.js stack from thin npm dep signal (issue #410, PR #411 floor + this override). Cache is session-stable — restart the MCP server to pick up edits. Empty file or read errors fall through to auto-detect (with stderr warning on errors). Files over 2 KB are clamped with a stderr warning.
234
+
235
+ **Tech-stack auto-detection.** When no override is present and the npm dep count is below `TECH_STACK_MIN_DEPS=3` OR the project is non-Node, `detectTechStack` scans the project root for known manifests (Cargo.toml, pyproject.toml, requirements.txt, go.mod, foundry.toml, Move.toml, Gemfile, composer.json), the README first 30 lines / 2 KB, and a shallow file-extension census (root only, excluding `node_modules`/`.git`/`.gossip`/`dist`/`build`/`out`/`coverage`, capped at 10 extension types; Config/docs extensions (`.json`, `.md`, `.yaml`, `.toml`, `.lock`, common dotfiles like `.prettierrc`/`.eslintrc`/`.dockerignore`/etc.) are excluded from the census — they're either ubiquitous across project types (carrying no toolchain signal) or already covered elsewhere (npm deps via `package.json`, language via manifests). Trade-off: Kubernetes / Ansible projects whose primary source is `.yaml` will receive less detection signal from the census; use `.gossip/tech-stack.md` for those). Any non-Node signal — manifest match, README content, or extension census — bypasses the `MIN_DEPS=3` floor so polyglot projects don't need the override file. Workspace-level manifests (e.g., `packages/contracts/foundry.toml`) are NOT scanned in this MVP; place the manifest at root or use `.gossip/tech-stack.md` for those cases.
236
+
231
237
  ### Verifying UNVERIFIED findings
232
238
 
233
239
  When a consensus report has `UNVERIFIED` findings (cross-reviewer couldn't check), **you must verify them yourself before presenting results**. UNVERIFIED means "the peer didn't have the tools or context to check" — you do. Read the cited files, grep for the identifiers, confirm or reject. Do not show raw consensus output with unexamined UNVERIFIED findings.
@@ -246,7 +252,7 @@ gossip_collect({
246
252
 
247
253
  Without `resolutionRoots`, citations to files that only exist in the worktree resolve against `projectRoot` → `⚠ file not found` → every cross-reviewer marks UNVERIFIED → the round produces zero verified findings. `resolutionRoots` runs each path through a validator (NUL reject, `..` reject, realpath, ownership check, git-common-dir match, `git worktree list` membership) before adding it to the citation-resolver trust zone.
248
254
 
249
- Secondary: `consensus.autoDiscoverWorktrees: true` in `.gossip/config.json` auto-discovers all git worktrees at round start (opt-in, default off) convenience for users who routinely run consensus on feature branches. Same validator applies.
255
+ Secondary: `consensus.autoDiscoverWorktrees: true` in `.gossip/config.json` is DISCOVERY-ONLY (opt-in, default off). When enabled, it logs a warning listing hashed paths of sibling git worktrees so operators know which branches exist — but it does NOT auto-route cross-reviewers to any of them. You must still pass explicit `resolutionRoots` to `gossip_dispatch` (or `gossip_collect`) to pin cross-reviewers to a specific worktree. Per consensus c6b8580d-595e48d2 + issue #402, prior auto-promotion behaviour was a foot-gun: it silently routed reviews to the wrong branch when multiple worktrees existed. Same validator applies to anything you do pass explicitly.
250
256
 
251
257
  See spec `docs/specs/2026-04-17-issue-126.md` for the full design.
252
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gossipcat",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
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": {