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.
- package/dist-mcp/mcp-server.js +146 -21
- package/docs/HANDBOOK.md +7 -1
- package/package.json +1 -1
package/dist-mcp/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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 = /<agent_finding\b/gi;
|
|
16474
16568
|
HTML_ENTITY_CLOSE_PATTERN = /<\/agent_finding>/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.
|
|
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.
|
|
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:
|
|
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 =
|
|
31029
|
-
const bTime =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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`
|
|
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