nexus-agents 2.125.21 → 2.125.24
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/{chunk-B6X3YEEP.js → chunk-647PSXJ5.js} +3 -3
- package/dist/{chunk-Q2EZE5MP.js → chunk-QAI5JEE2.js} +13 -1
- package/dist/{chunk-Q2EZE5MP.js.map → chunk-QAI5JEE2.js.map} +1 -1
- package/dist/{chunk-DCCDTUUP.js → chunk-QIEXH2EA.js} +2 -2
- package/dist/{chunk-5NG2GBNM.js → chunk-SDA5GEWO.js} +3 -3
- package/dist/cli.js +706 -28
- package/dist/cli.js.map +1 -1
- package/dist/{improvement-review-IR4BP23Z.js → improvement-review-L7MSBESK.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/{setup-command-553K2C2F.js → setup-command-63KWDRWL.js} +3 -3
- package/package.json +1 -1
- /package/dist/{chunk-B6X3YEEP.js.map → chunk-647PSXJ5.js.map} +0 -0
- /package/dist/{chunk-DCCDTUUP.js.map → chunk-QIEXH2EA.js.map} +0 -0
- /package/dist/{chunk-5NG2GBNM.js.map → chunk-SDA5GEWO.js.map} +0 -0
- /package/dist/{improvement-review-IR4BP23Z.js.map → improvement-review-L7MSBESK.js.map} +0 -0
- /package/dist/{setup-command-553K2C2F.js.map → setup-command-63KWDRWL.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
setupCommandAsync,
|
|
26
26
|
verifyCommand
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-QIEXH2EA.js";
|
|
28
28
|
import "./chunk-MMWNGT2K.js";
|
|
29
29
|
import {
|
|
30
30
|
AuthHandler,
|
|
@@ -144,7 +144,7 @@ import {
|
|
|
144
144
|
validateCommand,
|
|
145
145
|
validateWorkflow,
|
|
146
146
|
wrapInMarkdownFence
|
|
147
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-SDA5GEWO.js";
|
|
148
148
|
import "./chunk-3ACDP4E6.js";
|
|
149
149
|
import {
|
|
150
150
|
CATEGORY_DISPLAY_NAMES,
|
|
@@ -156,6 +156,7 @@ import {
|
|
|
156
156
|
shutdownExpertBridge
|
|
157
157
|
} from "./chunk-ZHBKDMHV.js";
|
|
158
158
|
import {
|
|
159
|
+
ConsensusVoteInputSchema,
|
|
159
160
|
DEFAULT_VOTE_TIMEOUT_MS,
|
|
160
161
|
ErrorPolicySchema,
|
|
161
162
|
VOTER_ROLES,
|
|
@@ -181,10 +182,12 @@ import {
|
|
|
181
182
|
import {
|
|
182
183
|
ImprovementReviewInputSchema,
|
|
183
184
|
calculateFitnessScore,
|
|
185
|
+
classifySignalPriority,
|
|
186
|
+
consensusFor,
|
|
184
187
|
createFitnessScoreCalculator,
|
|
185
188
|
registerImprovementReviewTool,
|
|
186
189
|
runImprovementReview
|
|
187
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-QAI5JEE2.js";
|
|
188
191
|
import {
|
|
189
192
|
createDefaultPolicyFirewall
|
|
190
193
|
} from "./chunk-K3MKXREZ.js";
|
|
@@ -243,7 +246,7 @@ import {
|
|
|
243
246
|
loadConfig,
|
|
244
247
|
runDoctor,
|
|
245
248
|
validateNexusEnv
|
|
246
|
-
} from "./chunk-
|
|
249
|
+
} from "./chunk-647PSXJ5.js";
|
|
247
250
|
import "./chunk-NUBSJGQZ.js";
|
|
248
251
|
import {
|
|
249
252
|
capitalize,
|
|
@@ -4027,12 +4030,12 @@ function formatTopsisRanking(result) {
|
|
|
4027
4030
|
lines.push(boxLine(color(" TOPSIS Ranking:", ANSI.bold)));
|
|
4028
4031
|
result.topsisResult.scores.forEach((score, idx) => {
|
|
4029
4032
|
const rank = idx + 1;
|
|
4030
|
-
const
|
|
4033
|
+
const pct2 = formatPercentage(score.closenessScore, 1);
|
|
4031
4034
|
const q = ((score.rawValues["quality"] ?? 0) * 10).toFixed(1);
|
|
4032
4035
|
const c = ((1 - (score.rawValues["cost"] ?? 0)) * 10).toFixed(1);
|
|
4033
4036
|
const l = ((1 - (score.rawValues["latency"] ?? 0)) * 10).toFixed(1);
|
|
4034
4037
|
lines.push(
|
|
4035
|
-
boxLine(` ${String(rank)}. ${score.cliName.padEnd(8)} (${
|
|
4038
|
+
boxLine(` ${String(rank)}. ${score.cliName.padEnd(8)} (${pct2}) q=${q} c=${c} l=${l}`)
|
|
4036
4039
|
);
|
|
4037
4040
|
});
|
|
4038
4041
|
lines.push(color("\u251C" + horizontalLine() + "\u2524", ANSI.cyan));
|
|
@@ -4087,10 +4090,10 @@ function formatExplorationSection(stats) {
|
|
|
4087
4090
|
color("\u2502", ANSI.yellow) + " Arm Distribution:".padEnd(BOX_WIDTH - 2) + color("\u2502", ANSI.yellow)
|
|
4088
4091
|
);
|
|
4089
4092
|
for (const arm of stats.exploration.armDistribution) {
|
|
4090
|
-
const
|
|
4093
|
+
const pct2 = formatPercentage(arm.proportion, 1);
|
|
4091
4094
|
const bar = "\u2588".repeat(Math.round(arm.proportion * 20));
|
|
4092
4095
|
lines.push(
|
|
4093
|
-
color("\u2502", ANSI.yellow) + ` ${arm.name.padEnd(8)} ${
|
|
4096
|
+
color("\u2502", ANSI.yellow) + ` ${arm.name.padEnd(8)} ${pct2.padStart(6)} ${bar}`.padEnd(BOX_WIDTH - 2) + color("\u2502", ANSI.yellow)
|
|
4094
4097
|
);
|
|
4095
4098
|
}
|
|
4096
4099
|
lines.push(color("\u251C" + horizontalLine() + "\u2524", ANSI.yellow));
|
|
@@ -4107,9 +4110,9 @@ function formatFeatureImportanceSection(stats) {
|
|
|
4107
4110
|
);
|
|
4108
4111
|
const top3 = arm.featureImportance.slice(0, 3);
|
|
4109
4112
|
for (const fi of top3) {
|
|
4110
|
-
const
|
|
4113
|
+
const pct2 = formatPercentage(fi.importance, 1);
|
|
4111
4114
|
lines.push(
|
|
4112
|
-
color("\u2502", ANSI.yellow) + ` ${fi.feature.padEnd(18)} ${
|
|
4115
|
+
color("\u2502", ANSI.yellow) + ` ${fi.feature.padEnd(18)} ${pct2.padStart(6)}`.padEnd(BOX_WIDTH - 2) + color("\u2502", ANSI.yellow)
|
|
4113
4116
|
);
|
|
4114
4117
|
}
|
|
4115
4118
|
}
|
|
@@ -9806,6 +9809,11 @@ var COMMAND_CATALOG = [
|
|
|
9806
9809
|
description: "Observability-driven improvement loop (#2402). Surfaces threshold breaches; --file-issues opt-in.",
|
|
9807
9810
|
audience: "advanced"
|
|
9808
9811
|
},
|
|
9812
|
+
{
|
|
9813
|
+
command: "auto-remediate",
|
|
9814
|
+
description: "Run one auto-remediation cycle (#3540). OFF unless NEXUS_AUTO_REMEDIATE=audit|enforce; never auto-merges.",
|
|
9815
|
+
audience: "maintainer"
|
|
9816
|
+
},
|
|
9809
9817
|
// ── Maintainer (hidden by default) ───────────────────────────────────────
|
|
9810
9818
|
{
|
|
9811
9819
|
command: "demo",
|
|
@@ -11324,10 +11332,10 @@ function formatModelStats(models) {
|
|
|
11324
11332
|
return lines;
|
|
11325
11333
|
}
|
|
11326
11334
|
for (const model of models) {
|
|
11327
|
-
const
|
|
11335
|
+
const pct2 = model.selectionPercent.toFixed(1);
|
|
11328
11336
|
const barLength = Math.min(20, Math.max(0, Math.round(model.selectionPercent * 0.2)));
|
|
11329
11337
|
const bar = "\u2588".repeat(barLength) + "\u2591".repeat(20 - barLength);
|
|
11330
|
-
lines.push(boxLine(` ${model.name.padEnd(10)} ${bar} ${
|
|
11338
|
+
lines.push(boxLine(` ${model.name.padEnd(10)} ${bar} ${pct2.padStart(5)}%`));
|
|
11331
11339
|
const reward = model.avgReward.toFixed(2);
|
|
11332
11340
|
const success = formatPercentage(model.successRate);
|
|
11333
11341
|
lines.push(boxLine(` reward: ${reward.padStart(5)} | success: ${success.padStart(4)}`));
|
|
@@ -17605,7 +17613,7 @@ function startImprovementReviewScheduler(options) {
|
|
|
17605
17613
|
let running = false;
|
|
17606
17614
|
const runOnce = async () => {
|
|
17607
17615
|
try {
|
|
17608
|
-
const { runImprovementReview: runImprovementReview2, ImprovementReviewInputSchema: ImprovementReviewInputSchema2 } = await import("./improvement-review-
|
|
17616
|
+
const { runImprovementReview: runImprovementReview2, ImprovementReviewInputSchema: ImprovementReviewInputSchema2 } = await import("./improvement-review-L7MSBESK.js");
|
|
17609
17617
|
const input = ImprovementReviewInputSchema2.parse({ fileIssues });
|
|
17610
17618
|
const result = await runImprovementReview2(input, { logger: logger19 });
|
|
17611
17619
|
logger19.info("Scheduled improvement_review complete", {
|
|
@@ -18039,8 +18047,8 @@ async function initUpstreamServers(gatewayConfig, server, logger19) {
|
|
|
18039
18047
|
servers: upstreamServers.length,
|
|
18040
18048
|
tools: tools.length
|
|
18041
18049
|
});
|
|
18042
|
-
const { z:
|
|
18043
|
-
const passthroughSchema =
|
|
18050
|
+
const { z: z9 } = await import("zod");
|
|
18051
|
+
const passthroughSchema = z9.looseObject({});
|
|
18044
18052
|
for (const tool of tools) {
|
|
18045
18053
|
const toolName = tool.name;
|
|
18046
18054
|
const desc = tool.description ?? `Upstream tool: ${toolName}`;
|
|
@@ -21617,29 +21625,29 @@ function checkThreshold(value, threshold, comparison, label, format) {
|
|
|
21617
21625
|
return `${label} ${format(value)} ${comparison === "min" ? "<" : ">"} ${format(threshold)}`;
|
|
21618
21626
|
}
|
|
21619
21627
|
function buildThresholdChecks(r, t) {
|
|
21620
|
-
const { pct, dec, ms, bytes } = formatters;
|
|
21628
|
+
const { pct: pct2, dec, ms, bytes } = formatters;
|
|
21621
21629
|
return [
|
|
21622
|
-
checkThreshold(r.recallAtK[5] ?? 0, t.minRecallAt5, "min", "Recall@5",
|
|
21623
|
-
checkThreshold(r.precisionAtK[5] ?? 0, t.minPrecisionAt5, "min", "Precision@5",
|
|
21630
|
+
checkThreshold(r.recallAtK[5] ?? 0, t.minRecallAt5, "min", "Recall@5", pct2),
|
|
21631
|
+
checkThreshold(r.precisionAtK[5] ?? 0, t.minPrecisionAt5, "min", "Precision@5", pct2),
|
|
21624
21632
|
checkThreshold(r.mrr, t.minMrr, "min", "MRR", dec),
|
|
21625
21633
|
checkThreshold(r.latencyP95Ms, t.maxLatencyP95Ms, "max", "P95 latency", ms),
|
|
21626
|
-
checkThreshold(r.coherenceScore, t.minCoherenceScore, "min", "Coherence",
|
|
21634
|
+
checkThreshold(r.coherenceScore, t.minCoherenceScore, "min", "Coherence", pct2),
|
|
21627
21635
|
checkThreshold(r.growthRateBytesPerOp, t.maxGrowthRateBytesPerOp, "max", "Growth rate", bytes),
|
|
21628
21636
|
checkThreshold(
|
|
21629
21637
|
r.decayConsistencyScore,
|
|
21630
21638
|
t.minDecayConsistencyScore,
|
|
21631
21639
|
"min",
|
|
21632
21640
|
"Decay consistency",
|
|
21633
|
-
|
|
21641
|
+
pct2
|
|
21634
21642
|
),
|
|
21635
21643
|
checkThreshold(
|
|
21636
21644
|
r.promotionRetentionRate,
|
|
21637
21645
|
t.minPromotionRetentionRate,
|
|
21638
21646
|
"min",
|
|
21639
21647
|
"Promotion retention",
|
|
21640
|
-
|
|
21648
|
+
pct2
|
|
21641
21649
|
),
|
|
21642
|
-
checkThreshold(r.decayRegretScore, t.maxDecayRegretScore, "max", "Decay regret",
|
|
21650
|
+
checkThreshold(r.decayRegretScore, t.maxDecayRegretScore, "max", "Decay regret", pct2)
|
|
21643
21651
|
];
|
|
21644
21652
|
}
|
|
21645
21653
|
function validateBenchmarkResults(result, thresholds) {
|
|
@@ -22562,19 +22570,19 @@ function renderMetricBar(value, max) {
|
|
|
22562
22570
|
}
|
|
22563
22571
|
function renderSwarmMetrics(w, health) {
|
|
22564
22572
|
const c = colors;
|
|
22565
|
-
const
|
|
22573
|
+
const pct2 = (v) => `${(v * 100).toFixed(1)}%`;
|
|
22566
22574
|
w(` ${c.bold}Swarm Health Metrics${c.reset}
|
|
22567
22575
|
`);
|
|
22568
22576
|
w(
|
|
22569
|
-
` Agent Utilization: ${renderMetricBar(health.agentUtilization, 1)} ${
|
|
22577
|
+
` Agent Utilization: ${renderMetricBar(health.agentUtilization, 1)} ${pct2(health.agentUtilization)}
|
|
22570
22578
|
`
|
|
22571
22579
|
);
|
|
22572
22580
|
w(
|
|
22573
|
-
` Collaboration Efficiency:${renderMetricBar(health.collaborationEfficiency, 1)} ${
|
|
22581
|
+
` Collaboration Efficiency:${renderMetricBar(health.collaborationEfficiency, 1)} ${pct2(health.collaborationEfficiency)}
|
|
22574
22582
|
`
|
|
22575
22583
|
);
|
|
22576
22584
|
w(
|
|
22577
|
-
` Routing Accuracy: ${renderMetricBar(health.routingAccuracy, 1)} ${
|
|
22585
|
+
` Routing Accuracy: ${renderMetricBar(health.routingAccuracy, 1)} ${pct2(health.routingAccuracy)}
|
|
22578
22586
|
`
|
|
22579
22587
|
);
|
|
22580
22588
|
w(` Weekly Regret: ${health.weeklyRegret.toFixed(3)}
|
|
@@ -22593,10 +22601,10 @@ function renderCliHealth(w, entries) {
|
|
|
22593
22601
|
`);
|
|
22594
22602
|
for (const entry of entries) {
|
|
22595
22603
|
const rateColor = entry.successRate >= 0.8 ? c.green : entry.successRate >= 0.6 ? c.yellow : c.red;
|
|
22596
|
-
const
|
|
22604
|
+
const pct2 = `${(entry.successRate * 100).toFixed(0)}%`;
|
|
22597
22605
|
const dur = entry.avgDurationMs >= 1e3 ? `${(entry.avgDurationMs / 1e3).toFixed(1)}s` : `${String(Math.round(entry.avgDurationMs))}ms`;
|
|
22598
22606
|
w(
|
|
22599
|
-
` ${entry.cli.padEnd(12)} ${rateColor}${
|
|
22607
|
+
` ${entry.cli.padEnd(12)} ${rateColor}${pct2.padStart(4)}${c.reset} ${String(entry.totalTasks).padStart(5)} tasks ${c.dim}avg ${dur}${c.reset}
|
|
22600
22608
|
`
|
|
22601
22609
|
);
|
|
22602
22610
|
}
|
|
@@ -22905,6 +22913,674 @@ function printSignal(signal) {
|
|
|
22905
22913
|
console.log("");
|
|
22906
22914
|
}
|
|
22907
22915
|
|
|
22916
|
+
// src/mcp/tools/remediation-research.ts
|
|
22917
|
+
var ACTION_BY_CATEGORY = {
|
|
22918
|
+
routing: "adjust-routing",
|
|
22919
|
+
bug: "fix-bug",
|
|
22920
|
+
"tech-debt": "refactor",
|
|
22921
|
+
security: "investigate",
|
|
22922
|
+
// conservative — security is p0/unanimous-gated regardless
|
|
22923
|
+
consensus: "investigate"
|
|
22924
|
+
};
|
|
22925
|
+
function clip(s, max) {
|
|
22926
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
22927
|
+
}
|
|
22928
|
+
function buildRemediationPlanFromSignal(signal) {
|
|
22929
|
+
return {
|
|
22930
|
+
signalKey: signal.signalKey,
|
|
22931
|
+
category: signal.category,
|
|
22932
|
+
summary: clip(`Remediate the surfaced signal: ${signal.title}`, 1e3),
|
|
22933
|
+
steps: [
|
|
22934
|
+
{
|
|
22935
|
+
kind: "investigate",
|
|
22936
|
+
description: clip(`Diagnose the root cause behind "${signal.title}".`, 500)
|
|
22937
|
+
},
|
|
22938
|
+
{
|
|
22939
|
+
kind: ACTION_BY_CATEGORY[signal.category],
|
|
22940
|
+
description: clip(
|
|
22941
|
+
`Address the ${signal.category} issue per the signal's evidence; keep the change minimal.`,
|
|
22942
|
+
500
|
|
22943
|
+
)
|
|
22944
|
+
},
|
|
22945
|
+
{
|
|
22946
|
+
kind: "add-test",
|
|
22947
|
+
description: "Add a regression test that fails without the fix and passes with it."
|
|
22948
|
+
}
|
|
22949
|
+
]
|
|
22950
|
+
};
|
|
22951
|
+
}
|
|
22952
|
+
|
|
22953
|
+
// src/mcp/tools/remediation-vote-adapter.ts
|
|
22954
|
+
function buildVoteInput(proposal, algorithm) {
|
|
22955
|
+
return ConsensusVoteInputSchema.parse({
|
|
22956
|
+
proposal,
|
|
22957
|
+
strategy: algorithm,
|
|
22958
|
+
simulateVotes: false
|
|
22959
|
+
// never gate auto-remediation on simulated votes
|
|
22960
|
+
});
|
|
22961
|
+
}
|
|
22962
|
+
function makeDefaultRunner(logger19) {
|
|
22963
|
+
return async (proposal, algorithm) => {
|
|
22964
|
+
const { result } = await executeVoting(buildVoteInput(proposal, algorithm), logger19);
|
|
22965
|
+
return {
|
|
22966
|
+
approved: result.outcome === "approved",
|
|
22967
|
+
approvalPercentage: result.approvalPercentage
|
|
22968
|
+
};
|
|
22969
|
+
};
|
|
22970
|
+
}
|
|
22971
|
+
function makeVoteAdapter(runner, logger19 = createLogger({ tool: "auto-remediation-vote" })) {
|
|
22972
|
+
const run = runner ?? makeDefaultRunner(logger19);
|
|
22973
|
+
return (input) => run(input.proposal, input.algorithm);
|
|
22974
|
+
}
|
|
22975
|
+
|
|
22976
|
+
// src/mcp/tools/auto-remediation-lease.ts
|
|
22977
|
+
import { execFile as execFile3 } from "child_process";
|
|
22978
|
+
import { promisify as promisify3 } from "util";
|
|
22979
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
22980
|
+
function lockRef(key) {
|
|
22981
|
+
return `refs/locks/${key}`;
|
|
22982
|
+
}
|
|
22983
|
+
var defaultGhRunner = async (args) => {
|
|
22984
|
+
try {
|
|
22985
|
+
const { stdout, stderr } = await execFileAsync3("gh", [...args]);
|
|
22986
|
+
return { exitCode: 0, stdout, stderr };
|
|
22987
|
+
} catch (err2) {
|
|
22988
|
+
const e = err2;
|
|
22989
|
+
return {
|
|
22990
|
+
exitCode: typeof e.code === "number" ? e.code : 1,
|
|
22991
|
+
stdout: e.stdout ?? "",
|
|
22992
|
+
stderr: e.stderr ?? e.message ?? "gh failed"
|
|
22993
|
+
};
|
|
22994
|
+
}
|
|
22995
|
+
};
|
|
22996
|
+
function makeGitRefLeaseAcquirer(opts) {
|
|
22997
|
+
const gh = opts.gh ?? defaultGhRunner;
|
|
22998
|
+
return async (key) => {
|
|
22999
|
+
const ref = lockRef(key);
|
|
23000
|
+
const res = await gh([
|
|
23001
|
+
"api",
|
|
23002
|
+
"-X",
|
|
23003
|
+
"POST",
|
|
23004
|
+
`repos/${opts.repo}/git/refs`,
|
|
23005
|
+
"-f",
|
|
23006
|
+
`ref=${ref}`,
|
|
23007
|
+
"-f",
|
|
23008
|
+
`sha=${opts.sha}`
|
|
23009
|
+
]);
|
|
23010
|
+
if (res.exitCode !== 0) {
|
|
23011
|
+
opts.logger?.info("auto-remediation lease not acquired (held or error) \u2014 fail-closed", {
|
|
23012
|
+
ref,
|
|
23013
|
+
stderr: res.stderr.slice(0, 200)
|
|
23014
|
+
});
|
|
23015
|
+
return null;
|
|
23016
|
+
}
|
|
23017
|
+
return {
|
|
23018
|
+
release: async () => {
|
|
23019
|
+
const del = await gh([
|
|
23020
|
+
"api",
|
|
23021
|
+
"-X",
|
|
23022
|
+
"DELETE",
|
|
23023
|
+
`repos/${opts.repo}/git/refs/${stripRefsPrefix(ref)}`
|
|
23024
|
+
]);
|
|
23025
|
+
if (del.exitCode !== 0) {
|
|
23026
|
+
opts.logger?.warn("auto-remediation lease release failed (stale lock; see #3646)", {
|
|
23027
|
+
ref,
|
|
23028
|
+
stderr: del.stderr.slice(0, 200)
|
|
23029
|
+
});
|
|
23030
|
+
}
|
|
23031
|
+
}
|
|
23032
|
+
};
|
|
23033
|
+
};
|
|
23034
|
+
}
|
|
23035
|
+
function stripRefsPrefix(ref) {
|
|
23036
|
+
return ref.replace(/^refs\//, "");
|
|
23037
|
+
}
|
|
23038
|
+
|
|
23039
|
+
// src/mcp/tools/auto-remediation-deps.ts
|
|
23040
|
+
var NOT_READY = {
|
|
23041
|
+
shadowSelections: 0,
|
|
23042
|
+
judgedSelections: 0,
|
|
23043
|
+
judgedSound: 0
|
|
23044
|
+
};
|
|
23045
|
+
function buildAutoRemediationDeps(opts = {}) {
|
|
23046
|
+
const logger19 = opts.logger ?? createLogger({ tool: "auto-remediation" });
|
|
23047
|
+
const acquireLease = opts.repo !== void 0 && opts.sha !== void 0 ? makeGitRefLeaseAcquirer({ repo: opts.repo, sha: opts.sha, logger: logger19 }) : (
|
|
23048
|
+
// Fail-closed: without a configured repo/sha we can't take the cross-process
|
|
23049
|
+
// lease, so enforce must not proceed. (Audit never calls this.)
|
|
23050
|
+
async () => Promise.resolve(null)
|
|
23051
|
+
);
|
|
23052
|
+
return {
|
|
23053
|
+
research: (signal) => Promise.resolve(buildRemediationPlanFromSignal(signal)),
|
|
23054
|
+
vote: makeVoteAdapter(opts.voteRunner, logger19),
|
|
23055
|
+
acquireLease,
|
|
23056
|
+
readinessEvidence: opts.readiness ?? (() => Promise.resolve(NOT_READY)),
|
|
23057
|
+
implement: () => Promise.reject(
|
|
23058
|
+
new Error(
|
|
23059
|
+
"auto-remediation implement adapter not wired yet (Option B, #3669) \u2014 enforce unavailable"
|
|
23060
|
+
)
|
|
23061
|
+
),
|
|
23062
|
+
audit: (event) => {
|
|
23063
|
+
logger19.info(`[auto-remediation] ${event.step}`, {
|
|
23064
|
+
...event.signalKey !== void 0 ? { signalKey: event.signalKey } : {},
|
|
23065
|
+
detail: event.detail
|
|
23066
|
+
});
|
|
23067
|
+
},
|
|
23068
|
+
logger: logger19
|
|
23069
|
+
};
|
|
23070
|
+
}
|
|
23071
|
+
|
|
23072
|
+
// src/mcp/tools/improvement-remediation-guard.ts
|
|
23073
|
+
var DEFAULT_REMEDIATION_GUARD_CONFIG = {
|
|
23074
|
+
cooldownMs: 6 * 60 * 60 * 1e3,
|
|
23075
|
+
// 6h between attempts on the same signal
|
|
23076
|
+
maxGenerations: 1,
|
|
23077
|
+
// a remediation may not spawn a remediation that spawns another
|
|
23078
|
+
maxPerWindow: 5,
|
|
23079
|
+
// mirror MAX_ISSUES_PER_RUN
|
|
23080
|
+
windowMs: 24 * 60 * 60 * 1e3,
|
|
23081
|
+
// per day
|
|
23082
|
+
maxHistory: 500
|
|
23083
|
+
};
|
|
23084
|
+
var RemediationGuard = class {
|
|
23085
|
+
config;
|
|
23086
|
+
attempts = [];
|
|
23087
|
+
constructor(config = {}) {
|
|
23088
|
+
this.config = { ...DEFAULT_REMEDIATION_GUARD_CONFIG, ...config };
|
|
23089
|
+
}
|
|
23090
|
+
/**
|
|
23091
|
+
* Decide whether a remediation for `signalKey` at `generation` may proceed at
|
|
23092
|
+
* time `now`. Pure read — does not record the attempt (call
|
|
23093
|
+
* {@link recordAttempt} only when the remediation actually proceeds).
|
|
23094
|
+
*/
|
|
23095
|
+
canRemediate(signalKey, now, generation = 0) {
|
|
23096
|
+
if (generation > this.config.maxGenerations) {
|
|
23097
|
+
return {
|
|
23098
|
+
allowed: false,
|
|
23099
|
+
blockReason: "depth",
|
|
23100
|
+
detail: `generation ${String(generation)} exceeds maxGenerations ${String(this.config.maxGenerations)} (runaway chain)`
|
|
23101
|
+
};
|
|
23102
|
+
}
|
|
23103
|
+
const last = this.lastAttempt(signalKey);
|
|
23104
|
+
if (last !== void 0 && now - last.timestamp < this.config.cooldownMs) {
|
|
23105
|
+
const waitMs = this.config.cooldownMs - (now - last.timestamp);
|
|
23106
|
+
return {
|
|
23107
|
+
allowed: false,
|
|
23108
|
+
blockReason: "cooldown",
|
|
23109
|
+
detail: `signal '${signalKey}' attempted ${String(Math.round((now - last.timestamp) / 1e3))}s ago; cooldown ${String(Math.round(this.config.cooldownMs / 1e3))}s (wait ${String(Math.round(waitMs / 1e3))}s)`
|
|
23110
|
+
};
|
|
23111
|
+
}
|
|
23112
|
+
const inWindow = this.countInWindow(now);
|
|
23113
|
+
if (inWindow >= this.config.maxPerWindow) {
|
|
23114
|
+
return {
|
|
23115
|
+
allowed: false,
|
|
23116
|
+
blockReason: "rate",
|
|
23117
|
+
detail: `${String(inWindow)} attempts in the last ${String(Math.round(this.config.windowMs / 1e3))}s reaches the cap of ${String(this.config.maxPerWindow)}`
|
|
23118
|
+
};
|
|
23119
|
+
}
|
|
23120
|
+
return { allowed: true, detail: "within cooldown, depth, and rate bounds" };
|
|
23121
|
+
}
|
|
23122
|
+
/** Record that a remediation proceeded. Bounded history (oldest evicted). */
|
|
23123
|
+
recordAttempt(signalKey, now, generation = 0) {
|
|
23124
|
+
this.attempts.push({ signalKey, timestamp: now, generation });
|
|
23125
|
+
if (this.attempts.length > this.config.maxHistory) {
|
|
23126
|
+
this.attempts.splice(0, this.attempts.length - this.config.maxHistory);
|
|
23127
|
+
}
|
|
23128
|
+
}
|
|
23129
|
+
/** Most-recent attempt for a signalKey, if any. */
|
|
23130
|
+
lastAttempt(signalKey) {
|
|
23131
|
+
let found;
|
|
23132
|
+
for (const a of this.attempts) {
|
|
23133
|
+
if (a.signalKey === signalKey && (found === void 0 || a.timestamp > found.timestamp)) {
|
|
23134
|
+
found = a;
|
|
23135
|
+
}
|
|
23136
|
+
}
|
|
23137
|
+
return found;
|
|
23138
|
+
}
|
|
23139
|
+
/** Number of attempts within the rate window ending at `now`. */
|
|
23140
|
+
countInWindow(now) {
|
|
23141
|
+
const cutoff = now - this.config.windowMs;
|
|
23142
|
+
return this.attempts.reduce((n, a) => a.timestamp >= cutoff ? n + 1 : n, 0);
|
|
23143
|
+
}
|
|
23144
|
+
};
|
|
23145
|
+
var singletonGuard;
|
|
23146
|
+
function getRemediationGuard() {
|
|
23147
|
+
singletonGuard ??= new RemediationGuard();
|
|
23148
|
+
return singletonGuard;
|
|
23149
|
+
}
|
|
23150
|
+
|
|
23151
|
+
// src/mcp/tools/improvement-enforce-readiness.ts
|
|
23152
|
+
var DEFAULT_ENFORCE_READINESS_CONFIG = {
|
|
23153
|
+
minShadowSelections: 20,
|
|
23154
|
+
minJudgedRate: 0.8,
|
|
23155
|
+
minSoundnessRate: 0.9,
|
|
23156
|
+
requireNamedEvaluator: true,
|
|
23157
|
+
requireNamedOwner: true
|
|
23158
|
+
};
|
|
23159
|
+
function pct(n, d) {
|
|
23160
|
+
return d === 0 ? 0 : n / d;
|
|
23161
|
+
}
|
|
23162
|
+
function presenceCriterion(name, label, value, required) {
|
|
23163
|
+
const present = value !== "";
|
|
23164
|
+
return {
|
|
23165
|
+
name,
|
|
23166
|
+
met: !required || present,
|
|
23167
|
+
detail: present ? `${label}: ${value}` : `no named ${label}`
|
|
23168
|
+
};
|
|
23169
|
+
}
|
|
23170
|
+
function evaluateEnforceReadiness(evidence, config = DEFAULT_ENFORCE_READINESS_CONFIG) {
|
|
23171
|
+
const judgedRate = pct(evidence.judgedSelections, evidence.shadowSelections);
|
|
23172
|
+
const soundnessRate = pct(evidence.judgedSound, evidence.judgedSelections);
|
|
23173
|
+
const evaluator = evidence.evaluator?.trim() ?? "";
|
|
23174
|
+
const owner = evidence.owner?.trim() ?? "";
|
|
23175
|
+
const criteria = [
|
|
23176
|
+
{
|
|
23177
|
+
name: "volume",
|
|
23178
|
+
met: evidence.shadowSelections >= config.minShadowSelections,
|
|
23179
|
+
detail: `${String(evidence.shadowSelections)} shadow selections (need \u2265 ${String(config.minShadowSelections)})`
|
|
23180
|
+
},
|
|
23181
|
+
{
|
|
23182
|
+
name: "judged-coverage",
|
|
23183
|
+
met: judgedRate >= config.minJudgedRate,
|
|
23184
|
+
detail: `${String(Math.round(judgedRate * 100))}% reviewed (need \u2265 ${String(Math.round(config.minJudgedRate * 100))}%)`
|
|
23185
|
+
},
|
|
23186
|
+
{
|
|
23187
|
+
name: "soundness",
|
|
23188
|
+
met: evidence.judgedSelections > 0 && soundnessRate >= config.minSoundnessRate,
|
|
23189
|
+
detail: `${String(Math.round(soundnessRate * 100))}% of reviewed judged sound (need \u2265 ${String(Math.round(config.minSoundnessRate * 100))}%, with reviews present)`
|
|
23190
|
+
},
|
|
23191
|
+
presenceCriterion("named-evaluator", "evaluator", evaluator, config.requireNamedEvaluator),
|
|
23192
|
+
presenceCriterion("named-owner", "owner", owner, config.requireNamedOwner)
|
|
23193
|
+
];
|
|
23194
|
+
const blockers = criteria.filter((c) => !c.met).map((c) => c.name);
|
|
23195
|
+
return { ready: blockers.length === 0, criteria, blockers };
|
|
23196
|
+
}
|
|
23197
|
+
|
|
23198
|
+
// src/mcp/tools/improvement-remediation-capability.ts
|
|
23199
|
+
import { z as z8 } from "zod";
|
|
23200
|
+
var PHASE_CAPABILITIES = {
|
|
23201
|
+
research: /* @__PURE__ */ new Set(["untrusted-input", "secrets"]),
|
|
23202
|
+
implement: /* @__PURE__ */ new Set(["repo-write", "secrets"])
|
|
23203
|
+
};
|
|
23204
|
+
var RuleOfTwoViolation = class extends Error {
|
|
23205
|
+
constructor(message) {
|
|
23206
|
+
super(message);
|
|
23207
|
+
this.name = "RuleOfTwoViolation";
|
|
23208
|
+
}
|
|
23209
|
+
};
|
|
23210
|
+
var CapabilityLedger = class {
|
|
23211
|
+
phase;
|
|
23212
|
+
/** Enter a phase, setting its declared capability set as active. */
|
|
23213
|
+
enterPhase(phase) {
|
|
23214
|
+
this.phase = phase;
|
|
23215
|
+
}
|
|
23216
|
+
/** The active phase, or undefined if none entered. */
|
|
23217
|
+
currentPhase() {
|
|
23218
|
+
return this.phase;
|
|
23219
|
+
}
|
|
23220
|
+
/**
|
|
23221
|
+
* Assert that `capability` is permitted right now. Throws {@link RuleOfTwoViolation}
|
|
23222
|
+
* (fail-closed) if no phase is active or the active phase doesn't grant it —
|
|
23223
|
+
* which structurally prevents the forbidden three-leg conjunction, since no
|
|
23224
|
+
* phase grants more than two legs.
|
|
23225
|
+
*/
|
|
23226
|
+
assertCapability(capability) {
|
|
23227
|
+
if (this.phase === void 0) {
|
|
23228
|
+
throw new RuleOfTwoViolation(
|
|
23229
|
+
`capability '${capability}' requested before any remediation phase was entered (fail-closed)`
|
|
23230
|
+
);
|
|
23231
|
+
}
|
|
23232
|
+
const allowed = PHASE_CAPABILITIES[this.phase];
|
|
23233
|
+
if (!allowed.has(capability)) {
|
|
23234
|
+
throw new RuleOfTwoViolation(
|
|
23235
|
+
`capability '${capability}' is not permitted in phase '${this.phase}' (allowed: ${[...allowed].join(", ")})`
|
|
23236
|
+
);
|
|
23237
|
+
}
|
|
23238
|
+
}
|
|
23239
|
+
};
|
|
23240
|
+
var RemediationActionKindSchema = z8.enum([
|
|
23241
|
+
"investigate",
|
|
23242
|
+
"adjust-routing",
|
|
23243
|
+
"add-test",
|
|
23244
|
+
"refactor",
|
|
23245
|
+
"update-docs",
|
|
23246
|
+
"fix-bug"
|
|
23247
|
+
]);
|
|
23248
|
+
var RemediationStepSchema = z8.object({
|
|
23249
|
+
kind: RemediationActionKindSchema,
|
|
23250
|
+
description: z8.string().min(1).max(500),
|
|
23251
|
+
/** Optional path hint; bounded inert string, validated for traversal at use. */
|
|
23252
|
+
targetPath: z8.string().min(1).max(300).optional()
|
|
23253
|
+
}).strict();
|
|
23254
|
+
var RemediationPlanSchema = z8.object({
|
|
23255
|
+
/** Source signal key (mirrors ImprovementSignal.signalKey). */
|
|
23256
|
+
signalKey: z8.string().min(1).max(200),
|
|
23257
|
+
/** Signal category (mirrors SignalCategory). */
|
|
23258
|
+
category: z8.enum(["routing", "tech-debt", "bug", "security", "consensus"]),
|
|
23259
|
+
summary: z8.string().min(1).max(1e3),
|
|
23260
|
+
steps: z8.array(RemediationStepSchema).min(1).max(20)
|
|
23261
|
+
}).strict();
|
|
23262
|
+
function parseRemediationPlan(raw) {
|
|
23263
|
+
const result = RemediationPlanSchema.safeParse(raw);
|
|
23264
|
+
if (!result.success) {
|
|
23265
|
+
throw new RuleOfTwoViolation(
|
|
23266
|
+
`RemediationPlan failed strict validation at the RESEARCH\u2192IMPLEMENT boundary: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
23267
|
+
);
|
|
23268
|
+
}
|
|
23269
|
+
return result.data;
|
|
23270
|
+
}
|
|
23271
|
+
function renderPlanAsResearch(plan) {
|
|
23272
|
+
const steps = plan.steps.map((s, i) => {
|
|
23273
|
+
const target = s.targetPath !== void 0 ? ` (target: ${s.targetPath})` : "";
|
|
23274
|
+
return `${String(i + 1)}. [${s.kind}] ${s.description}${target}`;
|
|
23275
|
+
}).join("\n");
|
|
23276
|
+
return `Remediation plan for signal '${plan.signalKey}' (category: ${plan.category}).
|
|
23277
|
+
|
|
23278
|
+
${plan.summary}
|
|
23279
|
+
|
|
23280
|
+
Steps:
|
|
23281
|
+
${steps}`;
|
|
23282
|
+
}
|
|
23283
|
+
|
|
23284
|
+
// src/mcp/tools/remediation-circuit-breaker.ts
|
|
23285
|
+
var DEFAULT_CIRCUIT_BREAKER_CONFIG = { threshold: 3 };
|
|
23286
|
+
var RemediationCircuitBreaker = class {
|
|
23287
|
+
threshold;
|
|
23288
|
+
consecutiveFailures = 0;
|
|
23289
|
+
tripped = false;
|
|
23290
|
+
constructor(config = {}) {
|
|
23291
|
+
this.threshold = config.threshold ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.threshold;
|
|
23292
|
+
}
|
|
23293
|
+
/** A remediation succeeded — clears the failure streak (does NOT un-trip). */
|
|
23294
|
+
recordSuccess() {
|
|
23295
|
+
this.consecutiveFailures = 0;
|
|
23296
|
+
}
|
|
23297
|
+
/** A remediation was rejected/failed — trips once the streak reaches the threshold. */
|
|
23298
|
+
recordFailure() {
|
|
23299
|
+
this.consecutiveFailures += 1;
|
|
23300
|
+
if (this.consecutiveFailures >= this.threshold) this.tripped = true;
|
|
23301
|
+
}
|
|
23302
|
+
/** Record by result (convenience). */
|
|
23303
|
+
record(result) {
|
|
23304
|
+
if (result === "success") this.recordSuccess();
|
|
23305
|
+
else this.recordFailure();
|
|
23306
|
+
}
|
|
23307
|
+
/** True once the breaker has tripped — the enforce path must auto-revert to off. */
|
|
23308
|
+
isTripped() {
|
|
23309
|
+
return this.tripped;
|
|
23310
|
+
}
|
|
23311
|
+
/** Re-enable after a consensus re-vote. Clears the trip and the streak. */
|
|
23312
|
+
reset() {
|
|
23313
|
+
this.tripped = false;
|
|
23314
|
+
this.consecutiveFailures = 0;
|
|
23315
|
+
}
|
|
23316
|
+
/** Snapshot for audit/telemetry. */
|
|
23317
|
+
state() {
|
|
23318
|
+
return {
|
|
23319
|
+
tripped: this.tripped,
|
|
23320
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
23321
|
+
threshold: this.threshold
|
|
23322
|
+
};
|
|
23323
|
+
}
|
|
23324
|
+
};
|
|
23325
|
+
var singleton;
|
|
23326
|
+
function getRemediationCircuitBreaker() {
|
|
23327
|
+
singleton ??= new RemediationCircuitBreaker();
|
|
23328
|
+
return singleton;
|
|
23329
|
+
}
|
|
23330
|
+
|
|
23331
|
+
// src/mcp/tools/remediation-protected-paths.ts
|
|
23332
|
+
var PROTECTED_PATH_FRAGMENTS = [
|
|
23333
|
+
// The capability-loop's own machinery (no self-modification).
|
|
23334
|
+
"improvement-remediation",
|
|
23335
|
+
"remediation-priority",
|
|
23336
|
+
"remediation-circuit-breaker",
|
|
23337
|
+
"remediation-protected-paths",
|
|
23338
|
+
"auto-remediation-lease",
|
|
23339
|
+
"improvement-enforce-readiness",
|
|
23340
|
+
"improvement-review",
|
|
23341
|
+
// Consensus / voter configuration (can't weaken its own judge).
|
|
23342
|
+
"src/consensus/",
|
|
23343
|
+
// Governance rules (Rule-of-Two, untrusted-input, etc.).
|
|
23344
|
+
".rules/",
|
|
23345
|
+
"claude.md",
|
|
23346
|
+
"agents.md",
|
|
23347
|
+
"codeowners",
|
|
23348
|
+
// CI / supply chain (secret exposure).
|
|
23349
|
+
".github/workflows/",
|
|
23350
|
+
// Security + auth + secrets + access-control.
|
|
23351
|
+
"src/security/",
|
|
23352
|
+
"token-resolver",
|
|
23353
|
+
"access-constraint",
|
|
23354
|
+
"secret",
|
|
23355
|
+
"credential"
|
|
23356
|
+
];
|
|
23357
|
+
function normalize2(path21) {
|
|
23358
|
+
return path21.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
23359
|
+
}
|
|
23360
|
+
function isProtectedPath(path21) {
|
|
23361
|
+
const p = normalize2(path21);
|
|
23362
|
+
return PROTECTED_PATH_FRAGMENTS.some((frag) => p.includes(frag));
|
|
23363
|
+
}
|
|
23364
|
+
function planTouchesProtectedPath(plan) {
|
|
23365
|
+
const hits = plan.steps.map((s) => s.targetPath).filter((t) => t !== void 0 && isProtectedPath(t));
|
|
23366
|
+
return { protected: hits.length > 0, paths: hits };
|
|
23367
|
+
}
|
|
23368
|
+
|
|
23369
|
+
// src/mcp/tools/improvement-remediation-enforce.ts
|
|
23370
|
+
var AUTO_REMEDIATE_ENV = "NEXUS_AUTO_REMEDIATE";
|
|
23371
|
+
var AUTO_REMEDIATE_LEASE_KEY = "auto-remediation";
|
|
23372
|
+
function resolveAutoRemediateMode(raw) {
|
|
23373
|
+
if (raw === "enforce") return "enforce";
|
|
23374
|
+
if (raw === "audit") return "audit";
|
|
23375
|
+
return "off";
|
|
23376
|
+
}
|
|
23377
|
+
var MAX_PER_RUN_DEFAULT = 5;
|
|
23378
|
+
async function runAutoRemediation(signals, deps, config = {}) {
|
|
23379
|
+
const mode = config.mode ?? resolveAutoRemediateMode(process.env[AUTO_REMEDIATE_ENV]);
|
|
23380
|
+
const base = { mode, considered: signals.length, skipped: [], plans: [], remediated: [] };
|
|
23381
|
+
if (mode === "off") return base;
|
|
23382
|
+
deps.logger?.info(`auto-remediation starting in '${mode}' mode`, { signals: signals.length });
|
|
23383
|
+
deps.audit({ step: "start", detail: `mode=${mode}, ${String(signals.length)} signals` });
|
|
23384
|
+
let lease = null;
|
|
23385
|
+
if (mode === "enforce") {
|
|
23386
|
+
const gate = await checkEnforceGates(deps, config);
|
|
23387
|
+
if (gate.abort !== void 0) {
|
|
23388
|
+
deps.audit({ step: "abort", detail: gate.abort });
|
|
23389
|
+
return { ...base, aborted: gate.abort };
|
|
23390
|
+
}
|
|
23391
|
+
lease = gate.lease;
|
|
23392
|
+
}
|
|
23393
|
+
try {
|
|
23394
|
+
return await admitAndExecute(signals, deps, config, mode);
|
|
23395
|
+
} finally {
|
|
23396
|
+
if (lease !== null) await lease.release();
|
|
23397
|
+
}
|
|
23398
|
+
}
|
|
23399
|
+
async function checkEnforceGates(deps, config) {
|
|
23400
|
+
const breaker = config.breaker ?? getRemediationCircuitBreaker();
|
|
23401
|
+
if (breaker.isTripped()) {
|
|
23402
|
+
return {
|
|
23403
|
+
abort: "circuit breaker tripped (sustained failures) \u2014 re-vote required to re-enable",
|
|
23404
|
+
lease: null
|
|
23405
|
+
};
|
|
23406
|
+
}
|
|
23407
|
+
const evidence = await deps.readinessEvidence();
|
|
23408
|
+
const readiness = evaluateEnforceReadiness(
|
|
23409
|
+
evidence,
|
|
23410
|
+
config.readinessConfig ?? DEFAULT_ENFORCE_READINESS_CONFIG
|
|
23411
|
+
);
|
|
23412
|
+
if (!readiness.ready) {
|
|
23413
|
+
return { abort: `not ready to enforce: ${readiness.blockers.join(", ")}`, lease: null };
|
|
23414
|
+
}
|
|
23415
|
+
const lease = await deps.acquireLease(AUTO_REMEDIATE_LEASE_KEY);
|
|
23416
|
+
if (lease === null) {
|
|
23417
|
+
return { abort: "another auto-remediation run holds the lease", lease: null };
|
|
23418
|
+
}
|
|
23419
|
+
return { lease };
|
|
23420
|
+
}
|
|
23421
|
+
async function admitAndExecute(signals, deps, config, mode) {
|
|
23422
|
+
const guard = config.guard ?? getRemediationGuard();
|
|
23423
|
+
const breaker = config.breaker ?? getRemediationCircuitBreaker();
|
|
23424
|
+
const now = config.now ?? 0;
|
|
23425
|
+
const maxPerRun = config.maxPerRun ?? MAX_PER_RUN_DEFAULT;
|
|
23426
|
+
const skipped = [];
|
|
23427
|
+
const plans = [];
|
|
23428
|
+
const remediated = [];
|
|
23429
|
+
for (const signal of signals) {
|
|
23430
|
+
if (plans.length >= maxPerRun) {
|
|
23431
|
+
skipped.push({
|
|
23432
|
+
signalKey: signal.signalKey,
|
|
23433
|
+
reason: `rate cap ${String(maxPerRun)} reached`
|
|
23434
|
+
});
|
|
23435
|
+
continue;
|
|
23436
|
+
}
|
|
23437
|
+
const o = await processSignal(signal, deps, mode, { guard, breaker, now });
|
|
23438
|
+
if (o.kind === "skip") {
|
|
23439
|
+
skipped.push({ signalKey: signal.signalKey, reason: o.reason });
|
|
23440
|
+
continue;
|
|
23441
|
+
}
|
|
23442
|
+
plans.push({ signalKey: signal.signalKey });
|
|
23443
|
+
if (o.kind === "remediated") remediated.push({ signalKey: signal.signalKey, ...o.pr });
|
|
23444
|
+
}
|
|
23445
|
+
return { mode, considered: signals.length, skipped, plans, remediated };
|
|
23446
|
+
}
|
|
23447
|
+
async function processSignal(signal, deps, mode, ctx) {
|
|
23448
|
+
const admission = admitSignal(signal, ctx.guard, ctx.now);
|
|
23449
|
+
if (!admission.admit) {
|
|
23450
|
+
deps.audit({ step: "skip", signalKey: signal.signalKey, detail: admission.reason });
|
|
23451
|
+
return { kind: "skip", reason: admission.reason };
|
|
23452
|
+
}
|
|
23453
|
+
const outcome = await executeOne(signal, deps, mode, {
|
|
23454
|
+
guard: ctx.guard,
|
|
23455
|
+
now: ctx.now,
|
|
23456
|
+
requirement: admission.requirement
|
|
23457
|
+
});
|
|
23458
|
+
if (mode === "enforce" && outcome.result !== void 0) ctx.breaker.record(outcome.result);
|
|
23459
|
+
if (outcome.error !== void 0) return { kind: "skip", reason: outcome.error };
|
|
23460
|
+
if (outcome.pr !== void 0) return { kind: "remediated", pr: outcome.pr };
|
|
23461
|
+
return { kind: "plan" };
|
|
23462
|
+
}
|
|
23463
|
+
function admitSignal(signal, guard, now) {
|
|
23464
|
+
const priority = classifySignalPriority(signal);
|
|
23465
|
+
const requirement = consensusFor(priority);
|
|
23466
|
+
if (!requirement.autoRemediate) {
|
|
23467
|
+
return { admit: false, reason: `${priority} \u2014 file-only (no auto-remediation)` };
|
|
23468
|
+
}
|
|
23469
|
+
const decision = guard.canRemediate(signal.signalKey, now);
|
|
23470
|
+
if (!decision.allowed) return { admit: false, reason: `runaway guard: ${decision.detail}` };
|
|
23471
|
+
return { admit: true, priority, requirement };
|
|
23472
|
+
}
|
|
23473
|
+
async function consensusGate(signal, plan, requirement, ledger, deps) {
|
|
23474
|
+
const algorithm = requirement.algorithm;
|
|
23475
|
+
if (algorithm === void 0) return "no consensus algorithm for tier";
|
|
23476
|
+
const proposal = `Auto-remediation for '${signal.signalKey}'.
|
|
23477
|
+
|
|
23478
|
+
${renderPlanAsResearch(plan)}`;
|
|
23479
|
+
const vote = await deps.vote({ proposal, algorithm });
|
|
23480
|
+
deps.audit({
|
|
23481
|
+
step: "vote",
|
|
23482
|
+
signalKey: signal.signalKey,
|
|
23483
|
+
detail: `${algorithm}: ${vote.approved ? "approved" : "rejected"} (${String(Math.round(vote.approvalPercentage))}%)`
|
|
23484
|
+
});
|
|
23485
|
+
if (!vote.approved) {
|
|
23486
|
+
return `consensus ${algorithm} not reached (${String(Math.round(vote.approvalPercentage))}%) \u2014 left as an issue`;
|
|
23487
|
+
}
|
|
23488
|
+
if (requirement.requiresDryRun) {
|
|
23489
|
+
if (deps.dryRun === void 0) return "p0 requires a dry-run capability (fail-closed)";
|
|
23490
|
+
const dry = await deps.dryRun(plan, ledger);
|
|
23491
|
+
deps.audit({
|
|
23492
|
+
step: "dry-run",
|
|
23493
|
+
signalKey: signal.signalKey,
|
|
23494
|
+
detail: dry.ok ? "ok" : dry.detail
|
|
23495
|
+
});
|
|
23496
|
+
if (!dry.ok) return `p0 dry-run failed: ${dry.detail}`;
|
|
23497
|
+
}
|
|
23498
|
+
return null;
|
|
23499
|
+
}
|
|
23500
|
+
async function executeOne(signal, deps, mode, ctx) {
|
|
23501
|
+
const { guard, now, requirement } = ctx;
|
|
23502
|
+
const ledger = new CapabilityLedger();
|
|
23503
|
+
ledger.enterPhase("research");
|
|
23504
|
+
let plan;
|
|
23505
|
+
try {
|
|
23506
|
+
plan = parseRemediationPlan(await deps.research(signal, ledger));
|
|
23507
|
+
} catch (err2) {
|
|
23508
|
+
const reason = `research/plan failed: ${err2 instanceof Error ? err2.message : String(err2)}`;
|
|
23509
|
+
deps.audit({ step: "research-failed", signalKey: signal.signalKey, detail: reason });
|
|
23510
|
+
return { error: reason };
|
|
23511
|
+
}
|
|
23512
|
+
deps.audit({
|
|
23513
|
+
step: "plan",
|
|
23514
|
+
signalKey: signal.signalKey,
|
|
23515
|
+
detail: `${String(plan.steps.length)} steps`
|
|
23516
|
+
});
|
|
23517
|
+
const protectedPaths = planTouchesProtectedPath(plan);
|
|
23518
|
+
if (protectedPaths.protected) {
|
|
23519
|
+
const reason = `protected path(s) ${protectedPaths.paths.join(", ")} \u2014 human attestation required`;
|
|
23520
|
+
deps.audit({ step: "protected-path", signalKey: signal.signalKey, detail: reason });
|
|
23521
|
+
return { error: reason };
|
|
23522
|
+
}
|
|
23523
|
+
const blocked = await consensusGate(signal, plan, requirement, ledger, deps);
|
|
23524
|
+
if (blocked !== null) return { error: blocked, result: "failure" };
|
|
23525
|
+
if (mode === "audit") return {};
|
|
23526
|
+
ledger.enterPhase("implement");
|
|
23527
|
+
const pr = await deps.implement(plan, ledger);
|
|
23528
|
+
guard.recordAttempt(signal.signalKey, now);
|
|
23529
|
+
deps.recordOutcome?.(plan, pr);
|
|
23530
|
+
deps.audit({ step: "pr-opened", signalKey: signal.signalKey, detail: pr.prUrl });
|
|
23531
|
+
return { pr, result: "success" };
|
|
23532
|
+
}
|
|
23533
|
+
|
|
23534
|
+
// src/mcp/tools/auto-remediation-cycle.ts
|
|
23535
|
+
function offResult() {
|
|
23536
|
+
return { mode: "off", considered: 0, skipped: [], plans: [], remediated: [] };
|
|
23537
|
+
}
|
|
23538
|
+
async function runAutoRemediationCycle(config = {}, inject = {}) {
|
|
23539
|
+
const logger19 = config.logger ?? createLogger({ tool: "auto-remediation" });
|
|
23540
|
+
const mode = config.mode ?? resolveAutoRemediateMode(process.env[AUTO_REMEDIATE_ENV]);
|
|
23541
|
+
if (mode === "off") {
|
|
23542
|
+
logger19.debug("auto-remediation off (NEXUS_AUTO_REMEDIATE unset/off) \u2014 no-op");
|
|
23543
|
+
return offResult();
|
|
23544
|
+
}
|
|
23545
|
+
const signals = await collectCycleSignals(inject, config, logger19);
|
|
23546
|
+
const deps = resolveCycleDeps(inject, config, logger19);
|
|
23547
|
+
logger19.info(`auto-remediation cycle: ${mode} over ${String(signals.length)} signals`);
|
|
23548
|
+
return runAutoRemediation(signals, deps, { mode });
|
|
23549
|
+
}
|
|
23550
|
+
async function collectCycleSignals(inject, config, logger19) {
|
|
23551
|
+
if (inject.collectSignals) return inject.collectSignals();
|
|
23552
|
+
const input = ImprovementReviewInputSchema.parse(
|
|
23553
|
+
config.lookbackDays !== void 0 ? { lookbackDays: config.lookbackDays } : {}
|
|
23554
|
+
);
|
|
23555
|
+
return (await runImprovementReview(input, { logger: logger19 })).signals;
|
|
23556
|
+
}
|
|
23557
|
+
function resolveCycleDeps(inject, config, logger19) {
|
|
23558
|
+
if (inject.deps) return inject.deps;
|
|
23559
|
+
return buildAutoRemediationDeps({
|
|
23560
|
+
...config.repo !== void 0 ? { repo: config.repo } : {},
|
|
23561
|
+
...config.sha !== void 0 ? { sha: config.sha } : {},
|
|
23562
|
+
logger: logger19
|
|
23563
|
+
});
|
|
23564
|
+
}
|
|
23565
|
+
|
|
23566
|
+
// src/cli/auto-remediate-command.ts
|
|
23567
|
+
function summarize2(r) {
|
|
23568
|
+
const head = `auto-remediation [${r.mode}]`;
|
|
23569
|
+
if (r.aborted !== void 0) return `${head}: aborted \u2014 ${r.aborted}
|
|
23570
|
+
`;
|
|
23571
|
+
return `${head}: ${String(r.considered)} considered \xB7 ${String(r.plans.length)} planned \xB7 ${String(r.remediated.length)} PR(s) \xB7 ${String(r.skipped.length)} skipped
|
|
23572
|
+
`;
|
|
23573
|
+
}
|
|
23574
|
+
async function handleAutoRemediateCommand(args) {
|
|
23575
|
+
const result = await runAutoRemediationCycle();
|
|
23576
|
+
if (args.options.format === "json") {
|
|
23577
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
23578
|
+
`);
|
|
23579
|
+
return;
|
|
23580
|
+
}
|
|
23581
|
+
process.stdout.write(summarize2(result));
|
|
23582
|
+
}
|
|
23583
|
+
|
|
22908
23584
|
// src/cli/migrate-command.ts
|
|
22909
23585
|
import { cpSync, existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
|
|
22910
23586
|
import { homedir } from "os";
|
|
@@ -23114,6 +23790,8 @@ var ASYNC_COMMAND_HANDLERS = {
|
|
|
23114
23790
|
usage: handleUsageCommand,
|
|
23115
23791
|
// Issue #2444: improvement-review command (observability-driven improvement loop)
|
|
23116
23792
|
"improvement-review": handleImprovementReviewCommand,
|
|
23793
|
+
// #3540 phase 3 / #3671: run one auto-remediation cycle (mode from NEXUS_AUTO_REMEDIATE).
|
|
23794
|
+
"auto-remediate": handleAutoRemediateCommand,
|
|
23117
23795
|
// Issue #2879 / epic #2872: migrate command (relocate homedir state per-repo)
|
|
23118
23796
|
migrate: handleMigrateCommand,
|
|
23119
23797
|
// #2305 / #2308 / #2311: Init Portable Command (async because --install spawns npm)
|