nexus-agents 2.125.21 → 2.125.25

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-YQARWBVL.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-F6VJEXXJ.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-B3TJNOKQ.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)}`));
@@ -12735,8 +12743,8 @@ async function exportSessionMetrics(storage, sessionId, exportPath) {
12735
12743
  const session = sessionResult.value;
12736
12744
  const metrics = buildMetricsObject(session, sessionId);
12737
12745
  if (exportPath !== void 0) {
12738
- const { writeFile: writeFile6 } = await import("fs/promises");
12739
- await writeFile6(exportPath, JSON.stringify(metrics, null, 2));
12746
+ const { writeFile: writeFile7 } = await import("fs/promises");
12747
+ await writeFile7(exportPath, JSON.stringify(metrics, null, 2));
12740
12748
  logger14.info("Metrics exported", { path: exportPath });
12741
12749
  } else {
12742
12750
  logger14.debug("Session metrics", summarizeMetricsForDebug(metrics));
@@ -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,19 +22913,836 @@ 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/remediation-proposal-pr.ts
23040
+ import { execFile as execFile4 } from "child_process";
23041
+ import { promisify as promisify4 } from "util";
23042
+ import { writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
23043
+ import { join as join18, dirname as dirname11 } from "path";
23044
+
23045
+ // src/mcp/tools/improvement-remediation-capability.ts
23046
+ import { z as z8 } from "zod";
23047
+ var PHASE_CAPABILITIES = {
23048
+ research: /* @__PURE__ */ new Set(["untrusted-input", "secrets"]),
23049
+ implement: /* @__PURE__ */ new Set(["repo-write", "secrets"])
23050
+ };
23051
+ var RuleOfTwoViolation = class extends Error {
23052
+ constructor(message) {
23053
+ super(message);
23054
+ this.name = "RuleOfTwoViolation";
23055
+ }
23056
+ };
23057
+ var CapabilityLedger = class {
23058
+ phase;
23059
+ /** Enter a phase, setting its declared capability set as active. */
23060
+ enterPhase(phase) {
23061
+ this.phase = phase;
23062
+ }
23063
+ /** The active phase, or undefined if none entered. */
23064
+ currentPhase() {
23065
+ return this.phase;
23066
+ }
23067
+ /**
23068
+ * Assert that `capability` is permitted right now. Throws {@link RuleOfTwoViolation}
23069
+ * (fail-closed) if no phase is active or the active phase doesn't grant it —
23070
+ * which structurally prevents the forbidden three-leg conjunction, since no
23071
+ * phase grants more than two legs.
23072
+ */
23073
+ assertCapability(capability) {
23074
+ if (this.phase === void 0) {
23075
+ throw new RuleOfTwoViolation(
23076
+ `capability '${capability}' requested before any remediation phase was entered (fail-closed)`
23077
+ );
23078
+ }
23079
+ const allowed = PHASE_CAPABILITIES[this.phase];
23080
+ if (!allowed.has(capability)) {
23081
+ throw new RuleOfTwoViolation(
23082
+ `capability '${capability}' is not permitted in phase '${this.phase}' (allowed: ${[...allowed].join(", ")})`
23083
+ );
23084
+ }
23085
+ }
23086
+ };
23087
+ var RemediationActionKindSchema = z8.enum([
23088
+ "investigate",
23089
+ "adjust-routing",
23090
+ "add-test",
23091
+ "refactor",
23092
+ "update-docs",
23093
+ "fix-bug"
23094
+ ]);
23095
+ var RemediationStepSchema = z8.object({
23096
+ kind: RemediationActionKindSchema,
23097
+ description: z8.string().min(1).max(500),
23098
+ /** Optional path hint; bounded inert string, validated for traversal at use. */
23099
+ targetPath: z8.string().min(1).max(300).optional()
23100
+ }).strict();
23101
+ var RemediationPlanSchema = z8.object({
23102
+ /** Source signal key (mirrors ImprovementSignal.signalKey). */
23103
+ signalKey: z8.string().min(1).max(200),
23104
+ /** Signal category (mirrors SignalCategory). */
23105
+ category: z8.enum(["routing", "tech-debt", "bug", "security", "consensus"]),
23106
+ summary: z8.string().min(1).max(1e3),
23107
+ steps: z8.array(RemediationStepSchema).min(1).max(20)
23108
+ }).strict();
23109
+ function parseRemediationPlan(raw) {
23110
+ const result = RemediationPlanSchema.safeParse(raw);
23111
+ if (!result.success) {
23112
+ throw new RuleOfTwoViolation(
23113
+ `RemediationPlan failed strict validation at the RESEARCH\u2192IMPLEMENT boundary: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
23114
+ );
23115
+ }
23116
+ return result.data;
23117
+ }
23118
+ function renderPlanAsResearch(plan) {
23119
+ const steps = plan.steps.map((s, i) => {
23120
+ const target = s.targetPath !== void 0 ? ` (target: ${s.targetPath})` : "";
23121
+ return `${String(i + 1)}. [${s.kind}] ${s.description}${target}`;
23122
+ }).join("\n");
23123
+ return `Remediation plan for signal '${plan.signalKey}' (category: ${plan.category}).
23124
+
23125
+ ${plan.summary}
23126
+
23127
+ Steps:
23128
+ ${steps}`;
23129
+ }
23130
+
23131
+ // src/mcp/tools/auto-remediation-branch.ts
23132
+ var AUTO_REMEDIATION_BRANCH_PREFIX = "auto-remediation/";
23133
+ function autoRemediationBranchName(signalKey) {
23134
+ const slug = signalKey.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
23135
+ return `${AUTO_REMEDIATION_BRANCH_PREFIX}${slug || "signal"}`;
23136
+ }
23137
+
23138
+ // src/mcp/tools/diff-secret-scan.ts
23139
+ var SECRET_PATTERNS = [
23140
+ { name: "private-key-block", re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/ },
23141
+ { name: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/ },
23142
+ { name: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
23143
+ { name: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
23144
+ { name: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
23145
+ { name: "openai-key", re: /\bsk-[A-Za-z0-9]{32,}\b/ },
23146
+ { name: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
23147
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
23148
+ // Generic `secret/token/password/api_key = "long-value"` assignments.
23149
+ {
23150
+ name: "generic-credential-assignment",
23151
+ re: /(?:api[_-]?key|secret|token|password|passwd|credential)\s*[:=]\s*["'][A-Za-z0-9/+_-]{16,}["']/i
23152
+ }
23153
+ ];
23154
+ function scanForSecrets(text) {
23155
+ const findings = [];
23156
+ const lines = text.split("\n");
23157
+ for (let i = 0; i < lines.length; i++) {
23158
+ const line = lines[i] ?? "";
23159
+ for (const { name, re } of SECRET_PATTERNS) {
23160
+ if (re.test(line)) findings.push({ pattern: name, line: i + 1 });
23161
+ }
23162
+ }
23163
+ return { clean: findings.length === 0, findings };
23164
+ }
23165
+ function describeSecretFindings(result) {
23166
+ if (result.clean) return "no secrets detected";
23167
+ return result.findings.map((f) => `${f.pattern}@L${String(f.line)}`).join(", ");
23168
+ }
23169
+
23170
+ // src/mcp/tools/remediation-proposal-pr.ts
23171
+ var execFileAsync4 = promisify4(execFile4);
23172
+ function buildProposalDoc(plan) {
23173
+ return `# Auto-remediation proposal \u2014 \`${plan.signalKey}\`
23174
+
23175
+ > Consensus-approved remediation **proposal** (not auto-implemented). A human or coder implements the steps below; this PR is the reviewable plan.
23176
+
23177
+ ${renderPlanAsResearch(plan)}
23178
+ `;
23179
+ }
23180
+ function planSlug(signalKey) {
23181
+ return autoRemediationBranchName(signalKey).replace(/^auto-remediation\//, "");
23182
+ }
23183
+ function makeProposalPrImplementAdapter(opts) {
23184
+ const scan = opts.scan ?? scanForSecrets;
23185
+ const baseBranch = opts.baseBranch ?? "main";
23186
+ const logger19 = opts.logger ?? createLogger({ tool: "auto-remediation-pr" });
23187
+ return async (plan, ledger) => {
23188
+ ledger.assertCapability("repo-write");
23189
+ const branch = autoRemediationBranchName(plan.signalKey);
23190
+ const doc = buildProposalDoc(plan);
23191
+ const result = scan(doc);
23192
+ if (!result.clean) {
23193
+ throw new Error(
23194
+ `proposal PR aborted \u2014 secrets in plan doc: ${describeSecretFindings(result)}`
23195
+ );
23196
+ }
23197
+ const worktree = await opts.ops.addWorktree(branch, baseBranch);
23198
+ try {
23199
+ await opts.ops.writeFileIn(worktree, `remediation-plans/${planSlug(plan.signalKey)}.md`, doc);
23200
+ await opts.ops.commitAll(worktree, `chore(auto-remediation): proposal for ${plan.signalKey}`);
23201
+ await opts.ops.pushBranch(worktree, branch);
23202
+ const prUrl = await opts.pr.createDraftPr({
23203
+ branch,
23204
+ baseBranch,
23205
+ title: `auto-remediation proposal: ${plan.signalKey}`,
23206
+ body: doc
23207
+ });
23208
+ logger19.info("auto-remediation proposal PR opened", { branch, prUrl });
23209
+ return { branch, prUrl };
23210
+ } finally {
23211
+ await opts.ops.removeWorktree(worktree).catch((err2) => {
23212
+ logger19.warn("worktree cleanup failed (non-fatal)", {
23213
+ worktree,
23214
+ error: err2 instanceof Error ? err2.message : String(err2)
23215
+ });
23216
+ });
23217
+ }
23218
+ };
23219
+ }
23220
+ function makeGitWorktreeOps(repoRoot) {
23221
+ const git = async (args, cwd) => {
23222
+ await execFileAsync4("git", [...args], { cwd });
23223
+ };
23224
+ return {
23225
+ async addWorktree(branch, baseBranch) {
23226
+ const path21 = join18(repoRoot, ".nexus-worktrees", branch.replace(/\//g, "_"));
23227
+ await git(["worktree", "add", "-b", branch, path21, baseBranch], repoRoot);
23228
+ return path21;
23229
+ },
23230
+ async writeFileIn(worktreePath, relPath, content) {
23231
+ const abs = join18(worktreePath, relPath);
23232
+ await mkdir5(dirname11(abs), { recursive: true });
23233
+ await writeFile6(abs, content, "utf8");
23234
+ },
23235
+ async commitAll(worktreePath, message) {
23236
+ await git(["add", "-A"], worktreePath);
23237
+ await git(["commit", "-m", message], worktreePath);
23238
+ },
23239
+ async pushBranch(worktreePath, branch) {
23240
+ await git(["push", "-u", "origin", branch], worktreePath);
23241
+ },
23242
+ async removeWorktree(worktreePath) {
23243
+ await git(["worktree", "remove", worktreePath, "--force"], repoRoot);
23244
+ }
23245
+ };
23246
+ }
23247
+ function makeGhPrCreator() {
23248
+ return {
23249
+ async createDraftPr({ branch, baseBranch, title, body }) {
23250
+ const { stdout } = await execFileAsync4("gh", [
23251
+ "pr",
23252
+ "create",
23253
+ "--draft",
23254
+ "--head",
23255
+ branch,
23256
+ "--base",
23257
+ baseBranch,
23258
+ "--title",
23259
+ title,
23260
+ "--body",
23261
+ body
23262
+ ]);
23263
+ return stdout.trim();
23264
+ }
23265
+ };
23266
+ }
23267
+
23268
+ // src/mcp/tools/auto-remediation-deps.ts
23269
+ var NOT_READY = {
23270
+ shadowSelections: 0,
23271
+ judgedSelections: 0,
23272
+ judgedSound: 0
23273
+ };
23274
+ function buildAutoRemediationDeps(opts = {}) {
23275
+ const logger19 = opts.logger ?? createLogger({ tool: "auto-remediation" });
23276
+ const acquireLease = opts.repo !== void 0 && opts.sha !== void 0 ? makeGitRefLeaseAcquirer({ repo: opts.repo, sha: opts.sha, logger: logger19 }) : (
23277
+ // Fail-closed: without a configured repo/sha we can't take the cross-process
23278
+ // lease, so enforce must not proceed. (Audit never calls this.)
23279
+ async () => Promise.resolve(null)
23280
+ );
23281
+ const implement = opts.repoRoot !== void 0 && opts.repo !== void 0 ? makeProposalPrImplementAdapter({
23282
+ ops: makeGitWorktreeOps(opts.repoRoot),
23283
+ pr: makeGhPrCreator(),
23284
+ ...opts.baseBranch !== void 0 ? { baseBranch: opts.baseBranch } : {},
23285
+ logger: logger19
23286
+ }) : () => Promise.reject(
23287
+ new Error(
23288
+ "auto-remediation implement not wired \u2014 set repo + repoRoot to enable Option B (#3669)"
23289
+ )
23290
+ );
23291
+ return {
23292
+ research: (signal) => Promise.resolve(buildRemediationPlanFromSignal(signal)),
23293
+ vote: makeVoteAdapter(opts.voteRunner, logger19),
23294
+ acquireLease,
23295
+ readinessEvidence: opts.readiness ?? (() => Promise.resolve(NOT_READY)),
23296
+ implement,
23297
+ audit: (event) => {
23298
+ logger19.info(`[auto-remediation] ${event.step}`, {
23299
+ ...event.signalKey !== void 0 ? { signalKey: event.signalKey } : {},
23300
+ detail: event.detail
23301
+ });
23302
+ },
23303
+ logger: logger19
23304
+ };
23305
+ }
23306
+
23307
+ // src/mcp/tools/improvement-remediation-guard.ts
23308
+ var DEFAULT_REMEDIATION_GUARD_CONFIG = {
23309
+ cooldownMs: 6 * 60 * 60 * 1e3,
23310
+ // 6h between attempts on the same signal
23311
+ maxGenerations: 1,
23312
+ // a remediation may not spawn a remediation that spawns another
23313
+ maxPerWindow: 5,
23314
+ // mirror MAX_ISSUES_PER_RUN
23315
+ windowMs: 24 * 60 * 60 * 1e3,
23316
+ // per day
23317
+ maxHistory: 500
23318
+ };
23319
+ var RemediationGuard = class {
23320
+ config;
23321
+ attempts = [];
23322
+ constructor(config = {}) {
23323
+ this.config = { ...DEFAULT_REMEDIATION_GUARD_CONFIG, ...config };
23324
+ }
23325
+ /**
23326
+ * Decide whether a remediation for `signalKey` at `generation` may proceed at
23327
+ * time `now`. Pure read — does not record the attempt (call
23328
+ * {@link recordAttempt} only when the remediation actually proceeds).
23329
+ */
23330
+ canRemediate(signalKey, now, generation = 0) {
23331
+ if (generation > this.config.maxGenerations) {
23332
+ return {
23333
+ allowed: false,
23334
+ blockReason: "depth",
23335
+ detail: `generation ${String(generation)} exceeds maxGenerations ${String(this.config.maxGenerations)} (runaway chain)`
23336
+ };
23337
+ }
23338
+ const last = this.lastAttempt(signalKey);
23339
+ if (last !== void 0 && now - last.timestamp < this.config.cooldownMs) {
23340
+ const waitMs = this.config.cooldownMs - (now - last.timestamp);
23341
+ return {
23342
+ allowed: false,
23343
+ blockReason: "cooldown",
23344
+ 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)`
23345
+ };
23346
+ }
23347
+ const inWindow = this.countInWindow(now);
23348
+ if (inWindow >= this.config.maxPerWindow) {
23349
+ return {
23350
+ allowed: false,
23351
+ blockReason: "rate",
23352
+ detail: `${String(inWindow)} attempts in the last ${String(Math.round(this.config.windowMs / 1e3))}s reaches the cap of ${String(this.config.maxPerWindow)}`
23353
+ };
23354
+ }
23355
+ return { allowed: true, detail: "within cooldown, depth, and rate bounds" };
23356
+ }
23357
+ /** Record that a remediation proceeded. Bounded history (oldest evicted). */
23358
+ recordAttempt(signalKey, now, generation = 0) {
23359
+ this.attempts.push({ signalKey, timestamp: now, generation });
23360
+ if (this.attempts.length > this.config.maxHistory) {
23361
+ this.attempts.splice(0, this.attempts.length - this.config.maxHistory);
23362
+ }
23363
+ }
23364
+ /** Most-recent attempt for a signalKey, if any. */
23365
+ lastAttempt(signalKey) {
23366
+ let found;
23367
+ for (const a of this.attempts) {
23368
+ if (a.signalKey === signalKey && (found === void 0 || a.timestamp > found.timestamp)) {
23369
+ found = a;
23370
+ }
23371
+ }
23372
+ return found;
23373
+ }
23374
+ /** Number of attempts within the rate window ending at `now`. */
23375
+ countInWindow(now) {
23376
+ const cutoff = now - this.config.windowMs;
23377
+ return this.attempts.reduce((n, a) => a.timestamp >= cutoff ? n + 1 : n, 0);
23378
+ }
23379
+ };
23380
+ var singletonGuard;
23381
+ function getRemediationGuard() {
23382
+ singletonGuard ??= new RemediationGuard();
23383
+ return singletonGuard;
23384
+ }
23385
+
23386
+ // src/mcp/tools/improvement-enforce-readiness.ts
23387
+ var DEFAULT_ENFORCE_READINESS_CONFIG = {
23388
+ minShadowSelections: 20,
23389
+ minJudgedRate: 0.8,
23390
+ minSoundnessRate: 0.9,
23391
+ requireNamedEvaluator: true,
23392
+ requireNamedOwner: true
23393
+ };
23394
+ function pct(n, d) {
23395
+ return d === 0 ? 0 : n / d;
23396
+ }
23397
+ function presenceCriterion(name, label, value, required) {
23398
+ const present = value !== "";
23399
+ return {
23400
+ name,
23401
+ met: !required || present,
23402
+ detail: present ? `${label}: ${value}` : `no named ${label}`
23403
+ };
23404
+ }
23405
+ function evaluateEnforceReadiness(evidence, config = DEFAULT_ENFORCE_READINESS_CONFIG) {
23406
+ const judgedRate = pct(evidence.judgedSelections, evidence.shadowSelections);
23407
+ const soundnessRate = pct(evidence.judgedSound, evidence.judgedSelections);
23408
+ const evaluator = evidence.evaluator?.trim() ?? "";
23409
+ const owner = evidence.owner?.trim() ?? "";
23410
+ const criteria = [
23411
+ {
23412
+ name: "volume",
23413
+ met: evidence.shadowSelections >= config.minShadowSelections,
23414
+ detail: `${String(evidence.shadowSelections)} shadow selections (need \u2265 ${String(config.minShadowSelections)})`
23415
+ },
23416
+ {
23417
+ name: "judged-coverage",
23418
+ met: judgedRate >= config.minJudgedRate,
23419
+ detail: `${String(Math.round(judgedRate * 100))}% reviewed (need \u2265 ${String(Math.round(config.minJudgedRate * 100))}%)`
23420
+ },
23421
+ {
23422
+ name: "soundness",
23423
+ met: evidence.judgedSelections > 0 && soundnessRate >= config.minSoundnessRate,
23424
+ detail: `${String(Math.round(soundnessRate * 100))}% of reviewed judged sound (need \u2265 ${String(Math.round(config.minSoundnessRate * 100))}%, with reviews present)`
23425
+ },
23426
+ presenceCriterion("named-evaluator", "evaluator", evaluator, config.requireNamedEvaluator),
23427
+ presenceCriterion("named-owner", "owner", owner, config.requireNamedOwner)
23428
+ ];
23429
+ const blockers = criteria.filter((c) => !c.met).map((c) => c.name);
23430
+ return { ready: blockers.length === 0, criteria, blockers };
23431
+ }
23432
+
23433
+ // src/mcp/tools/remediation-circuit-breaker.ts
23434
+ var DEFAULT_CIRCUIT_BREAKER_CONFIG = { threshold: 3 };
23435
+ var RemediationCircuitBreaker = class {
23436
+ threshold;
23437
+ consecutiveFailures = 0;
23438
+ tripped = false;
23439
+ constructor(config = {}) {
23440
+ this.threshold = config.threshold ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.threshold;
23441
+ }
23442
+ /** A remediation succeeded — clears the failure streak (does NOT un-trip). */
23443
+ recordSuccess() {
23444
+ this.consecutiveFailures = 0;
23445
+ }
23446
+ /** A remediation was rejected/failed — trips once the streak reaches the threshold. */
23447
+ recordFailure() {
23448
+ this.consecutiveFailures += 1;
23449
+ if (this.consecutiveFailures >= this.threshold) this.tripped = true;
23450
+ }
23451
+ /** Record by result (convenience). */
23452
+ record(result) {
23453
+ if (result === "success") this.recordSuccess();
23454
+ else this.recordFailure();
23455
+ }
23456
+ /** True once the breaker has tripped — the enforce path must auto-revert to off. */
23457
+ isTripped() {
23458
+ return this.tripped;
23459
+ }
23460
+ /** Re-enable after a consensus re-vote. Clears the trip and the streak. */
23461
+ reset() {
23462
+ this.tripped = false;
23463
+ this.consecutiveFailures = 0;
23464
+ }
23465
+ /** Snapshot for audit/telemetry. */
23466
+ state() {
23467
+ return {
23468
+ tripped: this.tripped,
23469
+ consecutiveFailures: this.consecutiveFailures,
23470
+ threshold: this.threshold
23471
+ };
23472
+ }
23473
+ };
23474
+ var singleton;
23475
+ function getRemediationCircuitBreaker() {
23476
+ singleton ??= new RemediationCircuitBreaker();
23477
+ return singleton;
23478
+ }
23479
+
23480
+ // src/mcp/tools/remediation-protected-paths.ts
23481
+ var PROTECTED_PATH_FRAGMENTS = [
23482
+ // The capability-loop's own machinery (no self-modification).
23483
+ "improvement-remediation",
23484
+ "remediation-priority",
23485
+ "remediation-circuit-breaker",
23486
+ "remediation-protected-paths",
23487
+ "auto-remediation-lease",
23488
+ "improvement-enforce-readiness",
23489
+ "improvement-review",
23490
+ // Consensus / voter configuration (can't weaken its own judge).
23491
+ "src/consensus/",
23492
+ // Governance rules (Rule-of-Two, untrusted-input, etc.).
23493
+ ".rules/",
23494
+ "claude.md",
23495
+ "agents.md",
23496
+ "codeowners",
23497
+ // CI / supply chain (secret exposure).
23498
+ ".github/workflows/",
23499
+ // Security + auth + secrets + access-control.
23500
+ "src/security/",
23501
+ "token-resolver",
23502
+ "access-constraint",
23503
+ "secret",
23504
+ "credential"
23505
+ ];
23506
+ function normalize2(path21) {
23507
+ return path21.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
23508
+ }
23509
+ function isProtectedPath(path21) {
23510
+ const p = normalize2(path21);
23511
+ return PROTECTED_PATH_FRAGMENTS.some((frag) => p.includes(frag));
23512
+ }
23513
+ function planTouchesProtectedPath(plan) {
23514
+ const hits = plan.steps.map((s) => s.targetPath).filter((t) => t !== void 0 && isProtectedPath(t));
23515
+ return { protected: hits.length > 0, paths: hits };
23516
+ }
23517
+
23518
+ // src/mcp/tools/improvement-remediation-enforce.ts
23519
+ var AUTO_REMEDIATE_ENV = "NEXUS_AUTO_REMEDIATE";
23520
+ var AUTO_REMEDIATE_LEASE_KEY = "auto-remediation";
23521
+ function resolveAutoRemediateMode(raw) {
23522
+ if (raw === "enforce") return "enforce";
23523
+ if (raw === "audit") return "audit";
23524
+ return "off";
23525
+ }
23526
+ var MAX_PER_RUN_DEFAULT = 5;
23527
+ async function runAutoRemediation(signals, deps, config = {}) {
23528
+ const mode = config.mode ?? resolveAutoRemediateMode(process.env[AUTO_REMEDIATE_ENV]);
23529
+ const base = { mode, considered: signals.length, skipped: [], plans: [], remediated: [] };
23530
+ if (mode === "off") return base;
23531
+ deps.logger?.info(`auto-remediation starting in '${mode}' mode`, { signals: signals.length });
23532
+ deps.audit({ step: "start", detail: `mode=${mode}, ${String(signals.length)} signals` });
23533
+ let lease = null;
23534
+ if (mode === "enforce") {
23535
+ const gate = await checkEnforceGates(deps, config);
23536
+ if (gate.abort !== void 0) {
23537
+ deps.audit({ step: "abort", detail: gate.abort });
23538
+ return { ...base, aborted: gate.abort };
23539
+ }
23540
+ lease = gate.lease;
23541
+ }
23542
+ try {
23543
+ return await admitAndExecute(signals, deps, config, mode);
23544
+ } finally {
23545
+ if (lease !== null) await lease.release();
23546
+ }
23547
+ }
23548
+ async function checkEnforceGates(deps, config) {
23549
+ const breaker = config.breaker ?? getRemediationCircuitBreaker();
23550
+ if (breaker.isTripped()) {
23551
+ return {
23552
+ abort: "circuit breaker tripped (sustained failures) \u2014 re-vote required to re-enable",
23553
+ lease: null
23554
+ };
23555
+ }
23556
+ const evidence = await deps.readinessEvidence();
23557
+ const readiness = evaluateEnforceReadiness(
23558
+ evidence,
23559
+ config.readinessConfig ?? DEFAULT_ENFORCE_READINESS_CONFIG
23560
+ );
23561
+ if (!readiness.ready) {
23562
+ return { abort: `not ready to enforce: ${readiness.blockers.join(", ")}`, lease: null };
23563
+ }
23564
+ const lease = await deps.acquireLease(AUTO_REMEDIATE_LEASE_KEY);
23565
+ if (lease === null) {
23566
+ return { abort: "another auto-remediation run holds the lease", lease: null };
23567
+ }
23568
+ return { lease };
23569
+ }
23570
+ async function admitAndExecute(signals, deps, config, mode) {
23571
+ const guard = config.guard ?? getRemediationGuard();
23572
+ const breaker = config.breaker ?? getRemediationCircuitBreaker();
23573
+ const now = config.now ?? 0;
23574
+ const maxPerRun = config.maxPerRun ?? MAX_PER_RUN_DEFAULT;
23575
+ const skipped = [];
23576
+ const plans = [];
23577
+ const remediated = [];
23578
+ for (const signal of signals) {
23579
+ if (plans.length >= maxPerRun) {
23580
+ skipped.push({
23581
+ signalKey: signal.signalKey,
23582
+ reason: `rate cap ${String(maxPerRun)} reached`
23583
+ });
23584
+ continue;
23585
+ }
23586
+ const o = await processSignal(signal, deps, mode, { guard, breaker, now });
23587
+ if (o.kind === "skip") {
23588
+ skipped.push({ signalKey: signal.signalKey, reason: o.reason });
23589
+ continue;
23590
+ }
23591
+ plans.push({ signalKey: signal.signalKey });
23592
+ if (o.kind === "remediated") remediated.push({ signalKey: signal.signalKey, ...o.pr });
23593
+ }
23594
+ return { mode, considered: signals.length, skipped, plans, remediated };
23595
+ }
23596
+ async function processSignal(signal, deps, mode, ctx) {
23597
+ const admission = admitSignal(signal, ctx.guard, ctx.now);
23598
+ if (!admission.admit) {
23599
+ deps.audit({ step: "skip", signalKey: signal.signalKey, detail: admission.reason });
23600
+ return { kind: "skip", reason: admission.reason };
23601
+ }
23602
+ const outcome = await executeOne(signal, deps, mode, {
23603
+ guard: ctx.guard,
23604
+ now: ctx.now,
23605
+ requirement: admission.requirement
23606
+ });
23607
+ if (mode === "enforce" && outcome.result !== void 0) ctx.breaker.record(outcome.result);
23608
+ if (outcome.error !== void 0) return { kind: "skip", reason: outcome.error };
23609
+ if (outcome.pr !== void 0) return { kind: "remediated", pr: outcome.pr };
23610
+ return { kind: "plan" };
23611
+ }
23612
+ function admitSignal(signal, guard, now) {
23613
+ const priority = classifySignalPriority(signal);
23614
+ const requirement = consensusFor(priority);
23615
+ if (!requirement.autoRemediate) {
23616
+ return { admit: false, reason: `${priority} \u2014 file-only (no auto-remediation)` };
23617
+ }
23618
+ const decision = guard.canRemediate(signal.signalKey, now);
23619
+ if (!decision.allowed) return { admit: false, reason: `runaway guard: ${decision.detail}` };
23620
+ return { admit: true, priority, requirement };
23621
+ }
23622
+ async function consensusGate(signal, plan, requirement, ledger, deps) {
23623
+ const algorithm = requirement.algorithm;
23624
+ if (algorithm === void 0) return "no consensus algorithm for tier";
23625
+ const proposal = `Auto-remediation for '${signal.signalKey}'.
23626
+
23627
+ ${renderPlanAsResearch(plan)}`;
23628
+ const vote = await deps.vote({ proposal, algorithm });
23629
+ deps.audit({
23630
+ step: "vote",
23631
+ signalKey: signal.signalKey,
23632
+ detail: `${algorithm}: ${vote.approved ? "approved" : "rejected"} (${String(Math.round(vote.approvalPercentage))}%)`
23633
+ });
23634
+ if (!vote.approved) {
23635
+ return `consensus ${algorithm} not reached (${String(Math.round(vote.approvalPercentage))}%) \u2014 left as an issue`;
23636
+ }
23637
+ if (requirement.requiresDryRun) {
23638
+ if (deps.dryRun === void 0) return "p0 requires a dry-run capability (fail-closed)";
23639
+ const dry = await deps.dryRun(plan, ledger);
23640
+ deps.audit({
23641
+ step: "dry-run",
23642
+ signalKey: signal.signalKey,
23643
+ detail: dry.ok ? "ok" : dry.detail
23644
+ });
23645
+ if (!dry.ok) return `p0 dry-run failed: ${dry.detail}`;
23646
+ }
23647
+ return null;
23648
+ }
23649
+ async function executeOne(signal, deps, mode, ctx) {
23650
+ const { guard, now, requirement } = ctx;
23651
+ const ledger = new CapabilityLedger();
23652
+ ledger.enterPhase("research");
23653
+ let plan;
23654
+ try {
23655
+ plan = parseRemediationPlan(await deps.research(signal, ledger));
23656
+ } catch (err2) {
23657
+ const reason = `research/plan failed: ${err2 instanceof Error ? err2.message : String(err2)}`;
23658
+ deps.audit({ step: "research-failed", signalKey: signal.signalKey, detail: reason });
23659
+ return { error: reason };
23660
+ }
23661
+ deps.audit({
23662
+ step: "plan",
23663
+ signalKey: signal.signalKey,
23664
+ detail: `${String(plan.steps.length)} steps`
23665
+ });
23666
+ const protectedPaths = planTouchesProtectedPath(plan);
23667
+ if (protectedPaths.protected) {
23668
+ const reason = `protected path(s) ${protectedPaths.paths.join(", ")} \u2014 human attestation required`;
23669
+ deps.audit({ step: "protected-path", signalKey: signal.signalKey, detail: reason });
23670
+ return { error: reason };
23671
+ }
23672
+ const blocked = await consensusGate(signal, plan, requirement, ledger, deps);
23673
+ if (blocked !== null) return { error: blocked, result: "failure" };
23674
+ if (mode === "audit") return {};
23675
+ ledger.enterPhase("implement");
23676
+ const pr = await deps.implement(plan, ledger);
23677
+ guard.recordAttempt(signal.signalKey, now);
23678
+ deps.recordOutcome?.(plan, pr);
23679
+ deps.audit({ step: "pr-opened", signalKey: signal.signalKey, detail: pr.prUrl });
23680
+ return { pr, result: "success" };
23681
+ }
23682
+
23683
+ // src/mcp/tools/auto-remediation-cycle.ts
23684
+ function offResult() {
23685
+ return { mode: "off", considered: 0, skipped: [], plans: [], remediated: [] };
23686
+ }
23687
+ async function runAutoRemediationCycle(config = {}, inject = {}) {
23688
+ const logger19 = config.logger ?? createLogger({ tool: "auto-remediation" });
23689
+ const mode = config.mode ?? resolveAutoRemediateMode(process.env[AUTO_REMEDIATE_ENV]);
23690
+ if (mode === "off") {
23691
+ logger19.debug("auto-remediation off (NEXUS_AUTO_REMEDIATE unset/off) \u2014 no-op");
23692
+ return offResult();
23693
+ }
23694
+ const signals = await collectCycleSignals(inject, config, logger19);
23695
+ const deps = resolveCycleDeps(inject, config, logger19);
23696
+ logger19.info(`auto-remediation cycle: ${mode} over ${String(signals.length)} signals`);
23697
+ return runAutoRemediation(signals, deps, { mode });
23698
+ }
23699
+ async function collectCycleSignals(inject, config, logger19) {
23700
+ if (inject.collectSignals) return inject.collectSignals();
23701
+ const input = ImprovementReviewInputSchema.parse(
23702
+ config.lookbackDays !== void 0 ? { lookbackDays: config.lookbackDays } : {}
23703
+ );
23704
+ return (await runImprovementReview(input, { logger: logger19 })).signals;
23705
+ }
23706
+ function resolveCycleDeps(inject, config, logger19) {
23707
+ if (inject.deps) return inject.deps;
23708
+ return buildAutoRemediationDeps({
23709
+ ...config.repo !== void 0 ? { repo: config.repo } : {},
23710
+ ...config.sha !== void 0 ? { sha: config.sha } : {},
23711
+ logger: logger19
23712
+ });
23713
+ }
23714
+
23715
+ // src/cli/auto-remediate-command.ts
23716
+ function summarize2(r) {
23717
+ const head = `auto-remediation [${r.mode}]`;
23718
+ if (r.aborted !== void 0) return `${head}: aborted \u2014 ${r.aborted}
23719
+ `;
23720
+ 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
23721
+ `;
23722
+ }
23723
+ async function handleAutoRemediateCommand(args) {
23724
+ const result = await runAutoRemediationCycle();
23725
+ if (args.options.format === "json") {
23726
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
23727
+ `);
23728
+ return;
23729
+ }
23730
+ process.stdout.write(summarize2(result));
23731
+ }
23732
+
22908
23733
  // src/cli/migrate-command.ts
22909
23734
  import { cpSync, existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
22910
23735
  import { homedir } from "os";
22911
- import { join as join18 } from "path";
23736
+ import { join as join19 } from "path";
22912
23737
  var logger18 = createLogger({ component: "migrate-command" });
22913
- var HOMEDIR_DEFAULT_BASE = join18(homedir(), ".nexus-agents");
23738
+ var HOMEDIR_DEFAULT_BASE = join19(homedir(), ".nexus-agents");
22914
23739
  function countItems(dir) {
22915
23740
  if (!existsSync22(dir)) return 0;
22916
23741
  try {
22917
23742
  const entries = readdirSync3(dir, { withFileTypes: true });
22918
23743
  let n = 0;
22919
23744
  for (const e of entries) {
22920
- const p = join18(dir, e.name);
23745
+ const p = join19(dir, e.name);
22921
23746
  if (e.isDirectory()) {
22922
23747
  n += countItems(p);
22923
23748
  } else {
@@ -22972,8 +23797,8 @@ function checkEarlyExits(fromBase, toBase, dryRun) {
22972
23797
  return null;
22973
23798
  }
22974
23799
  function planAndExecuteEntry(name, fromBase, toBase, perRepoSet, dryRun) {
22975
- const source = join18(fromBase, name);
22976
- const target = join18(toBase, name);
23800
+ const source = join19(fromBase, name);
23801
+ const target = join19(toBase, name);
22977
23802
  if (!perRepoSet.has(name)) {
22978
23803
  return { subdir: name, status: "skipped-not-per-repo", source, target: "", itemsCopied: 0 };
22979
23804
  }
@@ -22993,7 +23818,7 @@ function planAndExecuteEntry(name, fromBase, toBase, perRepoSet, dryRun) {
22993
23818
  function resolveDefaultTarget(cwd) {
22994
23819
  const repoRoot = findRepoRoot(cwd);
22995
23820
  if (repoRoot === null) return null;
22996
- return join18(repoRoot, ".nexus-agents");
23821
+ return join19(repoRoot, ".nexus-agents");
22997
23822
  }
22998
23823
  function formatMigrationResult(result) {
22999
23824
  if (!result.success) {
@@ -23114,6 +23939,8 @@ var ASYNC_COMMAND_HANDLERS = {
23114
23939
  usage: handleUsageCommand,
23115
23940
  // Issue #2444: improvement-review command (observability-driven improvement loop)
23116
23941
  "improvement-review": handleImprovementReviewCommand,
23942
+ // #3540 phase 3 / #3671: run one auto-remediation cycle (mode from NEXUS_AUTO_REMEDIATE).
23943
+ "auto-remediate": handleAutoRemediateCommand,
23117
23944
  // Issue #2879 / epic #2872: migrate command (relocate homedir state per-repo)
23118
23945
  migrate: handleMigrateCommand,
23119
23946
  // #2305 / #2308 / #2311: Init Portable Command (async because --install spawns npm)