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/cli.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  import {
25
25
  setupCommandAsync,
26
26
  verifyCommand
27
- } from "./chunk-DCCDTUUP.js";
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-5NG2GBNM.js";
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-Q2EZE5MP.js";
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-B6X3YEEP.js";
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 pct = formatPercentage(score.closenessScore, 1);
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)} (${pct}) q=${q} c=${c} l=${l}`)
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 pct = formatPercentage(arm.proportion, 1);
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)} ${pct.padStart(6)} ${bar}`.padEnd(BOX_WIDTH - 2) + color("\u2502", ANSI.yellow)
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 pct = formatPercentage(fi.importance, 1);
4113
+ const pct2 = formatPercentage(fi.importance, 1);
4111
4114
  lines.push(
4112
- color("\u2502", ANSI.yellow) + ` ${fi.feature.padEnd(18)} ${pct.padStart(6)}`.padEnd(BOX_WIDTH - 2) + color("\u2502", ANSI.yellow)
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 pct = model.selectionPercent.toFixed(1);
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} ${pct.padStart(5)}%`));
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-IR4BP23Z.js");
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: z8 } = await import("zod");
18043
- const passthroughSchema = z8.looseObject({});
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", pct),
21623
- checkThreshold(r.precisionAtK[5] ?? 0, t.minPrecisionAt5, "min", "Precision@5", pct),
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", pct),
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
- pct
21641
+ pct2
21634
21642
  ),
21635
21643
  checkThreshold(
21636
21644
  r.promotionRetentionRate,
21637
21645
  t.minPromotionRetentionRate,
21638
21646
  "min",
21639
21647
  "Promotion retention",
21640
- pct
21648
+ pct2
21641
21649
  ),
21642
- checkThreshold(r.decayRegretScore, t.maxDecayRegretScore, "max", "Decay regret", pct)
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 pct = (v) => `${(v * 100).toFixed(1)}%`;
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)} ${pct(health.agentUtilization)}
22577
+ ` Agent Utilization: ${renderMetricBar(health.agentUtilization, 1)} ${pct2(health.agentUtilization)}
22570
22578
  `
22571
22579
  );
22572
22580
  w(
22573
- ` Collaboration Efficiency:${renderMetricBar(health.collaborationEfficiency, 1)} ${pct(health.collaborationEfficiency)}
22581
+ ` Collaboration Efficiency:${renderMetricBar(health.collaborationEfficiency, 1)} ${pct2(health.collaborationEfficiency)}
22574
22582
  `
22575
22583
  );
22576
22584
  w(
22577
- ` Routing Accuracy: ${renderMetricBar(health.routingAccuracy, 1)} ${pct(health.routingAccuracy)}
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 pct = `${(entry.successRate * 100).toFixed(0)}%`;
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}${pct.padStart(4)}${c.reset} ${String(entry.totalTasks).padStart(5)} tasks ${c.dim}avg ${dur}${c.reset}
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)