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.
- package/dist-mcp/mcp-server.js +45 -14
- package/docs/HANDBOOK.md +2 -2
- package/package.json +1 -1
package/dist-mcp/mcp-server.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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`
|
|
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