gossipcat 0.4.27 → 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.
@@ -10597,7 +10597,7 @@ ${clamped}`);
10597
10597
  }
10598
10598
  }
10599
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"]);
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
10601
  let extensionSignal = false;
10602
10602
  try {
10603
10603
  const entries = (0, import_fs9.readdirSync)(this.projectRoot, { withFileTypes: true });
@@ -16562,7 +16562,7 @@ var init_parse_findings = __esm({
16562
16562
  TYPE_ATTR_PATTERN = /type="([a-zA-Z]+)"/;
16563
16563
  SEVERITY_ATTR_PATTERN = /severity="(critical|high|medium|low)"/;
16564
16564
  CATEGORY_ATTR_PATTERN = /category="([a-z_]+)"/;
16565
- 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+/;
16566
16566
  CANONICAL_TYPES = /* @__PURE__ */ new Set(["finding", "suggestion", "insight"]);
16567
16567
  HTML_ENTITY_OPEN_PATTERN = /<agent_finding\b/gi;
16568
16568
  HTML_ENTITY_CLOSE_PATTERN = /<\/agent_finding>/gi;
@@ -16736,7 +16736,7 @@ var init_consensus_engine = __esm({
16736
16736
  };
16737
16737
  MIN_FINDINGS_PER_PEER = 2;
16738
16738
  VALID_ACTIONS = /* @__PURE__ */ new Set(["agree", "disagree", "unverified", "new"]);
16739
- 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+/;
16740
16740
  MAX_VERIFIER_TURNS = 7;
16741
16741
  VERIFIER_TOOLS = [
16742
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"] } },
@@ -17762,7 +17762,7 @@ Return only valid JSON.${skillsBlock}`;
17762
17762
  */
17763
17763
  async snippetsForFinding(findingText, maxSnippets = 3) {
17764
17764
  if (!this.config.projectRoot && this.currentWorktreeRoots.size === 0) return "";
17765
- 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;
17766
17766
  const CONTEXT_LINES = 2;
17767
17767
  const MAX_FILE_SIZE2 = 10 * 1024 * 1024;
17768
17768
  const anchors = [];
@@ -17783,7 +17783,7 @@ Return only valid JSON.${skillsBlock}`;
17783
17783
  anchors.push(`\u26A0 Agent cited \`${safeRef}:${lineNum}\` but file not found`);
17784
17784
  continue;
17785
17785
  }
17786
- const resolvedFromProjectRoot = this.currentWorktreeRoots.size > 0 && this.config.projectRoot && filePath.startsWith((0, import_path28.resolve)(this.config.projectRoot) + "/");
17786
+ const resolvedFromProjectRoot = this.isResolvedFromProjectRootOnly(filePath);
17787
17787
  const fileStat = await (0, import_promises3.stat)(filePath);
17788
17788
  if (fileStat.size > MAX_FILE_SIZE2) continue;
17789
17789
  const content = await this.cachedRead(filePath);
@@ -17817,7 +17817,7 @@ ${safeSnippet}
17817
17817
  const trimmed = value.trim();
17818
17818
  if (!trimmed || trimmed.length > 80) continue;
17819
17819
  if (tag === "file") {
17820
- 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+)$/);
17821
17821
  if (fileMatch && !seen.has(trimmed)) {
17822
17822
  seen.add(trimmed);
17823
17823
  const fullRef = fileMatch[1];
@@ -17827,7 +17827,7 @@ ${safeSnippet}
17827
17827
  const safeRef = fullRef.replace(/["<>]/g, "");
17828
17828
  const filePath = await this.cachedResolveForAnchor(fullRef) ?? await this.cachedResolveForAnchor(bareFile);
17829
17829
  if (filePath) {
17830
- const resolvedFromProjectRoot = this.currentWorktreeRoots.size > 0 && this.config.projectRoot && filePath.startsWith((0, import_path28.resolve)(this.config.projectRoot) + "/");
17830
+ const resolvedFromProjectRoot = this.isResolvedFromProjectRootOnly(filePath);
17831
17831
  const content = await this.cachedRead(filePath);
17832
17832
  if (content) {
17833
17833
  const fileLines = content.split("\n");
@@ -17907,7 +17907,7 @@ ${safeSnippet}
17907
17907
  return false;
17908
17908
  }
17909
17909
  const stripped = evidence.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "").replace(/<example>[\s\S]*?<\/example>/gi, "").replace(/"[^"\n]*"/g, "").replace(/'[^'\n]*'/g, "");
17910
- 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;
17911
17911
  const rawCitations = [];
17912
17912
  let match;
17913
17913
  while ((match = citationPattern.exec(stripped)) !== null) {
@@ -17953,6 +17953,28 @@ ${safeSnippet}
17953
17953
  * worktree can still be auto-anchored when consensus runs back in the main
17954
17954
  * MCP process).
17955
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
+ }
17956
17978
  /**
17957
17979
  * Guard: resolved path must stay inside one of the valid roots, with
17958
17980
  * symlink-safe containment.
@@ -19157,7 +19179,7 @@ function detectFormatCompliance(result) {
19157
19179
  const tags_dropped_unknown_type = Object.values(parseRes.droppedUnknownType).reduce((a, b) => a + b, 0) + parseRes.droppedMissingType;
19158
19180
  const tags_dropped_short_content = parseRes.droppedShortContent;
19159
19181
  const findingCount = (body.match(/<agent_finding[\s>]/g) ?? []).length;
19160
- 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;
19161
19183
  const formatCompliant = tags_accepted > 0 && citationCount >= tags_accepted;
19162
19184
  const diagnostics = [...parseRes.diagnostics];
19163
19185
  if (truncated) {
@@ -23407,7 +23429,7 @@ var init_dedupe_key = __esm({
23407
23429
  "packages/orchestrator/src/dedupe-key.ts"() {
23408
23430
  "use strict";
23409
23431
  import_crypto15 = require("crypto");
23410
- 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+/;
23411
23433
  MIN_NORMALIZED_CONTENT_LENGTH = 32;
23412
23434
  DEDUPE_KEY_INTERNALS = {
23413
23435
  MIN_NORMALIZED_CONTENT_LENGTH,
@@ -49431,7 +49453,10 @@ async function resolveDispatchResolutionRoots(dispatchResolutionRoots) {
49431
49453
  );
49432
49454
  }
49433
49455
  if (discovered.length > 0) {
49434
- 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
+ );
49435
49460
  } else if (rejected.length > 0) {
49436
49461
  warnings.push(
49437
49462
  `autoDiscoverWorktrees: ${rejected.length} candidate(s) failed validation; cross-review will use projectRoot only. See stderr for rejection reasons.`
@@ -50140,10 +50165,10 @@ ${t.skillWarnings.map((w) => ` - ${w}`).join("\n")}`;
50140
50165
  if (!mainLlm) {
50141
50166
  return { content: [{ type: "text", text: "Error: No LLM configured for consensus. Check gossip_setup." }] };
50142
50167
  }
50143
- 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));
50144
50169
  const performanceReader = new PerformanceReader3(process.cwd());
50145
50170
  const explicitRoots = resolutionRoots ?? [];
50146
- let effectiveRoots = explicitRoots;
50171
+ const effectiveRoots = explicitRoots;
50147
50172
  try {
50148
50173
  const { findConfigPath: findConfigPath2, loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
50149
50174
  const cfgPath = findConfigPath2(process.cwd());
@@ -50156,7 +50181,13 @@ ${t.skillWarnings.map((w) => ` - ${w}`).join("\n")}`;
50156
50181
  `
50157
50182
  );
50158
50183
  }
50159
- 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
+ }
50160
50191
  }
50161
50192
  } catch (err) {
50162
50193
  process.stderr.write(`[consensus] auto-discovery failed: ${err.message}
package/docs/HANDBOOK.md CHANGED
@@ -232,7 +232,7 @@ When blocked, the error message shows age + remaining cooldown + override instru
232
232
 
233
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
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 such as `.json`, `.md`, `.yaml`, `.toml`, `.lock` are excluded from the census since they are already captured by other signals). 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.
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
236
 
237
237
  ### Verifying UNVERIFIED findings
238
238
 
@@ -252,7 +252,7 @@ gossip_collect({
252
252
 
253
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.
254
254
 
255
- 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.
256
256
 
257
257
  See spec `docs/specs/2026-04-17-issue-126.md` for the full design.
258
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gossipcat",
3
- "version": "0.4.27",
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": {