@antonbabenko/deliberation-mcp 3.6.1 → 3.7.0

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.
Files changed (2) hide show
  1. package/dist/index.js +61 -52
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -241,7 +241,10 @@ var require_provider = __commonJS({
241
241
  var STRIP_TRAIL = /[*_`~\s]+$/;
242
242
  var HEADING_RE = /^#{1,6}\s/;
243
243
  function normVerdict(tok) {
244
- return tok.replace(/\s+/g, "_").toUpperCase();
244
+ return (
245
+ /** @type {any} */
246
+ tok.replace(/\s+/g, "_").toUpperCase()
247
+ );
245
248
  }
246
249
  function parseReview(text) {
247
250
  const raw = safeString(text);
@@ -259,36 +262,24 @@ var require_provider = __commonJS({
259
262
  function resolveVerdict(lines) {
260
263
  for (const ln of lines) {
261
264
  const m = ln.match(SENTINEL_RE);
262
- if (m) return (
263
- /** @type {any} */
264
- normVerdict(m[1])
265
- );
265
+ if (m) return normVerdict(m[1]);
266
266
  }
267
267
  for (const ln of lines) {
268
268
  const m = ln.match(VERDICT_RE);
269
- if (m) return (
270
- /** @type {any} */
271
- normVerdict(m[1])
272
- );
269
+ if (m) return normVerdict(m[1]);
273
270
  }
274
271
  for (let i = 0; i < lines.length; i++) {
275
272
  if (!VERDICT_WORD_RE.test(lines[i].trim())) continue;
276
273
  for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
277
274
  const t = lines[j].replace(MD_EMPHASIS, "").trim();
278
275
  if (!t) continue;
279
- if (TOKEN_LINE_RE.test(t)) return (
280
- /** @type {any} */
281
- normVerdict(t)
282
- );
276
+ if (TOKEN_LINE_RE.test(t)) return normVerdict(t);
283
277
  break;
284
278
  }
285
279
  }
286
- for (const ln of lines) {
287
- const t = ln.replace(MD_EMPHASIS, "").trim();
288
- if (TOKEN_LINE_RE.test(t)) return (
289
- /** @type {any} */
290
- normVerdict(t)
291
- );
280
+ const nonEmpty = lines.map((l) => l.replace(MD_EMPHASIS, "").trim()).filter((t) => t !== "");
281
+ for (const t of [nonEmpty[0], nonEmpty[nonEmpty.length - 1]]) {
282
+ if (t && TOKEN_LINE_RE.test(t)) return normVerdict(t);
292
283
  }
293
284
  return null;
294
285
  }
@@ -345,6 +336,7 @@ var require_consensus_loop = __commonJS({
345
336
  "use strict";
346
337
  var MAX_ROUNDS_DEFAULT = 5;
347
338
  var VERDICTS = Object.freeze(["APPROVE", "REQUEST_CHANGES", "REJECT"]);
339
+ var REVIEW_FORMAT_INSTRUCTION = "End with a line by itself in exactly this form (no markdown, token on the SAME line): VERDICT: APPROVE (or VERDICT: REQUEST_CHANGES, or VERDICT: REJECT). Then list any critical issues, one per line as: - [category] description where category is one of security, correctness, scope, ambiguity, performance, ops.";
348
340
  function assertStatus(state, expected, op) {
349
341
  if (state.status !== expected) {
350
342
  throw new Error(`${op}: expected status '${expected}', got '${state.status}'`);
@@ -387,12 +379,12 @@ ${state.currentPlan}`
387
379
  const peerPrompt = [
388
380
  body,
389
381
  "Review the plan for correctness, security, scope, ambiguity, performance, and ops gaps.",
390
- "End with a line by itself in exactly this form (no markdown, token on the SAME line): VERDICT: APPROVE (or VERDICT: REQUEST_CHANGES, or VERDICT: REJECT). Then list any critical issues, one per line as: - [category] description where category is one of security, correctness, scope, ambiguity, performance, ops."
382
+ REVIEW_FORMAT_INSTRUCTION
391
383
  ].join("\n\n");
392
384
  const blindPrompt = [
393
385
  body,
394
386
  "Give your own independent verdict BEFORE seeing peer opinions.",
395
- "End with a line by itself in exactly this form (no markdown, token on the SAME line): VERDICT: APPROVE (or VERDICT: REQUEST_CHANGES, or VERDICT: REJECT). Then list any critical issues, one per line as: - [category] description where category is one of security, correctness, scope, ambiguity, performance, ops."
387
+ REVIEW_FORMAT_INSTRUCTION
396
388
  ].join("\n\n");
397
389
  return { peerPrompt, blindPrompt };
398
390
  }
@@ -1118,7 +1110,7 @@ var require_sessions = __commonJS({
1118
1110
  var DEFAULT_MAX_AGE_DAYS = 30;
1119
1111
  function scrubSecrets(text) {
1120
1112
  if (typeof text !== "string" || text.length === 0) return text;
1121
- return text.replace(/\bsk-or-[A-Za-z0-9_-]{20,}/g, "[REDACTED]").replace(/\bsk-[A-Za-z0-9_-]{20,}/g, "[REDACTED]").replace(/\bxai-[A-Za-z0-9_-]{20,}/g, "[REDACTED]").replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "[REDACTED]").replace(/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED]").replace(/\bAIza[0-9A-Za-z_-]{35,}/g, "[REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/-]{20,}={0,2}/g, "Bearer [REDACTED]");
1113
+ return text.replace(/\bsk-or-[A-Za-z0-9_-]{20,}/g, "[REDACTED]").replace(/\bsk-[A-Za-z0-9_-]{20,}/g, "[REDACTED]").replace(/\bxai-[A-Za-z0-9_-]{20,}/g, "[REDACTED]").replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "[REDACTED]").replace(/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED]").replace(/\bAIza[0-9A-Za-z_-]{35,}/g, "[REDACTED]").replace(/\b([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:)[^\s@/]{6,}@/gi, "$1[REDACTED]@").replace(/\bToken\s+[A-Za-z0-9._~+/-]{20,}={0,2}/g, "Token [REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/-]{20,}={0,2}/g, "Bearer [REDACTED]");
1122
1114
  }
1123
1115
  function capText(text) {
1124
1116
  if (typeof text !== "string") return text;
@@ -1699,24 +1691,27 @@ var require_antigravity = __commonJS({
1699
1691
  );
1700
1692
  if (!bridge) throw new Error("makeAntigravityProvider requires opts.bridge (core is transport-agnostic; inject the gemini bridge)");
1701
1693
  const model = opts.model || process.env.GEMINI_DEFAULT_MODEL || "auto-gemini-3";
1694
+ const allowImplement = opts.allowImplement === true;
1702
1695
  return {
1703
1696
  name: "gemini",
1704
- capabilities: { canImplement: true, fileUpload: false, multiTurn: true, walksFilesystem: true },
1697
+ // canImplement reflects the construction lock so discovery (panel) is honest about THIS process.
1698
+ capabilities: { canImplement: allowImplement, fileUpload: false, multiTurn: true, walksFilesystem: true },
1705
1699
  async health() {
1706
1700
  return typeof bridge.runGemini === "function" ? { ok: true } : { ok: false, reason: "agy bridge unavailable" };
1707
1701
  },
1708
1702
  async ask(req) {
1709
1703
  const started = Date.now();
1704
+ const implement = allowImplement && req.mode === "implement";
1710
1705
  const includeDirs = (req.files || []).filter((f) => f.dir).map((f) => f.dir);
1711
1706
  const args = bridge.buildAgyArgs({
1712
1707
  prompt: req.prompt,
1713
1708
  model,
1714
- sandbox: "read-only",
1709
+ sandbox: implement ? "workspace-write" : "read-only",
1715
1710
  developerInstructions: req.developerInstructions,
1716
1711
  includeDirs
1717
1712
  });
1718
1713
  try {
1719
- const out = await bridge.runGemini(args, req.cwd, req.timeoutMs, void 0, { readOnly: true, includeDirs });
1714
+ const out = await bridge.runGemini(args, req.cwd, req.timeoutMs, void 0, { readOnly: !implement, includeDirs });
1720
1715
  return { provider: "gemini", model, text: out.response || "", threadId: out.threadId, isError: false, ms: Date.now() - started, reasoningEffort: null, ...out.workspaceMutated ? { workspaceMutated: true } : {} };
1721
1716
  } catch (e) {
1722
1717
  const err = (
@@ -1751,12 +1746,13 @@ var require_codex = __commonJS({
1751
1746
  if (s.includes("rate")) return { errorKind: "rate-limit", retryable: true };
1752
1747
  return { errorKind: "unknown", retryable: false };
1753
1748
  }
1754
- function codexExecArgs() {
1755
- return ["exec", "--sandbox", "read-only", "--skip-git-repo-check"];
1749
+ function codexExecArgs(mode) {
1750
+ const sandbox = mode === "implement" ? "workspace-write" : "read-only";
1751
+ return ["exec", "--sandbox", sandbox, "--skip-git-repo-check"];
1756
1752
  }
1757
- function defaultRun({ prompt, cwd, timeoutMs }) {
1753
+ function defaultRun({ prompt, cwd, timeoutMs, mode }) {
1758
1754
  return new Promise((resolve) => {
1759
- const child = spawn("codex", codexExecArgs(), { cwd: cwd || process.cwd() });
1755
+ const child = spawn("codex", codexExecArgs(mode), { cwd: cwd || process.cwd() });
1760
1756
  let stdout = "", stderr = "", settled = false;
1761
1757
  const timer = timeoutMs ? setTimeout(() => child.kill("SIGKILL"), timeoutMs) : null;
1762
1758
  if (timer) timer.unref();
@@ -1780,21 +1776,24 @@ var require_codex = __commonJS({
1780
1776
  function makeCodexProvider(opts = {}) {
1781
1777
  const run = opts.run || defaultRun;
1782
1778
  const model = opts.model || "default";
1779
+ const allowImplement = opts.allowImplement === true;
1783
1780
  return {
1784
1781
  name: "codex",
1785
- capabilities: { canImplement: true, fileUpload: false, multiTurn: false, walksFilesystem: true },
1786
- // Option A: no threadId continuity
1782
+ // canImplement reflects the construction lock so discovery (panel) is honest about THIS
1783
+ // process. Option A: no threadId continuity (multiTurn:false).
1784
+ capabilities: { canImplement: allowImplement, fileUpload: false, multiTurn: false, walksFilesystem: true },
1787
1785
  async health() {
1788
1786
  return { ok: true };
1789
1787
  },
1790
1788
  async ask(req) {
1791
1789
  const started = Date.now();
1790
+ const mode = allowImplement && req.mode === "implement" ? "implement" : "advisory";
1792
1791
  const full = req.developerInstructions ? `${req.developerInstructions}
1793
1792
 
1794
1793
  ---
1795
1794
 
1796
1795
  ${req.prompt}` : req.prompt;
1797
- const { code, stdout, stderr } = await run({ prompt: full, cwd: req.cwd, timeoutMs: req.timeoutMs });
1796
+ const { code, stdout, stderr } = await run({ prompt: full, cwd: req.cwd, timeoutMs: req.timeoutMs, mode });
1798
1797
  if (code === 0) {
1799
1798
  return { provider: "codex", model, text: stdout.trim(), isError: false, ms: Date.now() - started, reasoningEffort: null };
1800
1799
  }
@@ -2202,6 +2201,7 @@ var require_gemini = __commonJS({
2202
2201
  var DEFAULT_RECOVERY_GRACE_MS = 12e4;
2203
2202
  var MAX_MS = 6e5;
2204
2203
  var VALID_SANDBOX_VALUES = /* @__PURE__ */ new Set(["read-only", "workspace-write"]);
2204
+ var THREAD_ID_RE = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,127}$/;
2205
2205
  function goDuration(ms) {
2206
2206
  return Math.ceil(ms / 1e3) + "s";
2207
2207
  }
@@ -2224,11 +2224,15 @@ ${prompt}`;
2224
2224
  args.push("-p", prompt);
2225
2225
  return args;
2226
2226
  }
2227
- var ADVISORY_ENV_SCRUB = ["GITHUB_TOKEN", "GH_TOKEN", "GIT_ASKPASS", "SSH_AUTH_SOCK"];
2227
+ var ADVISORY_ENV_SCRUB = ["GIT_ASKPASS", "SSH_AUTH_SOCK"];
2228
+ var CREDENTIAL_NAME_RE = /(?:^|_)(?:KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|CREDENTIAL|CREDENTIALS)$|API_KEY|ACCESS_KEY|SESSION_TOKEN|PRIVATE_KEY/i;
2228
2229
  function advisoryEnv(env) {
2229
2230
  const out = { ...env };
2230
2231
  delete out.DELIBERATION_DISABLE_OS_SANDBOX;
2231
2232
  for (const k of ADVISORY_ENV_SCRUB) delete out[k];
2233
+ for (const k of Object.keys(out)) {
2234
+ if (CREDENTIAL_NAME_RE.test(k)) delete out[k];
2235
+ }
2232
2236
  return out;
2233
2237
  }
2234
2238
  function seatbeltLiteral(p) {
@@ -2349,7 +2353,8 @@ ${prompt}`;
2349
2353
  real = fs.realpathSync(resolved);
2350
2354
  } catch (_) {
2351
2355
  }
2352
- return map[real] ?? map[resolved] ?? map[cwd] ?? null;
2356
+ const candidate = map[real] ?? map[resolved] ?? map[cwd] ?? null;
2357
+ return typeof candidate === "string" && THREAD_ID_RE.test(candidate) ? candidate : null;
2353
2358
  } catch (_) {
2354
2359
  return null;
2355
2360
  }
@@ -2391,7 +2396,10 @@ ${prompt}`;
2391
2396
  process.stderr.write("[deliberation] agy read-only run wrapped in sandbox-exec (workspace writes denied)\n");
2392
2397
  }
2393
2398
  const agyProcess = spawn(spawnPlan.cmd, spawnPlan.argv, {
2394
- env: readOnly ? advisoryEnv(process.env) : process.env,
2399
+ // Scrub credentials in BOTH read-only and workspace-write runs. A write run
2400
+ // legitimately mutates the worktree but still has no need for the operator's
2401
+ // API keys / GIT_ASKPASS / SSH_AUTH_SOCK - the human commits and pushes, not agy.
2402
+ env: advisoryEnv(process.env),
2395
2403
  shell: false,
2396
2404
  cwd: effCwd,
2397
2405
  // agy -p (print mode) waits for stdin EOF before returning; if the stdin
@@ -2674,8 +2682,8 @@ ${prompt}`;
2674
2682
  return;
2675
2683
  }
2676
2684
  const threadId2 = args.threadId.trim();
2677
- if (threadId2 === "" || threadId2 === "latest" || threadId2 === "unknown") {
2678
- if (shouldRespond) sendError(id, -32602, "Invalid params: 'threadId' must be an explicit conversation id, not '" + threadId2 + "'");
2685
+ if (!THREAD_ID_RE.test(threadId2) || threadId2 === "latest" || threadId2 === "unknown") {
2686
+ if (shouldRespond) sendError(id, -32602, "Invalid params: 'threadId' must be an explicit conversation id (alphanumeric/underscore start, then [A-Za-z0-9_-], 1..128 chars)");
2679
2687
  return;
2680
2688
  }
2681
2689
  if (!isNonEmptyString(args.prompt)) {
@@ -2772,6 +2780,7 @@ ${prompt}`;
2772
2780
  module2.exports.READ_ONLY_GUARD = READ_ONLY_GUARD;
2773
2781
  module2.exports.applyReadOnlyGuard = applyReadOnlyGuard;
2774
2782
  module2.exports.advisoryEnv = advisoryEnv;
2783
+ module2.exports.THREAD_ID_RE = THREAD_ID_RE;
2775
2784
  module2.exports.buildSeatbeltProfile = buildSeatbeltProfile;
2776
2785
  module2.exports.buildSpawnCommand = buildSpawnCommand;
2777
2786
  module2.exports.diffGitState = diffGitState;
@@ -2806,7 +2815,7 @@ var require_lock = __commonJS({
2806
2815
  try {
2807
2816
  fs.mkdirSync(lockDir);
2808
2817
  const markerPath = path.join(lockDir, markerName);
2809
- fs.writeFileSync(markerPath, JSON.stringify({ pid: process.pid, token, t: Date.now() }));
2818
+ fs.writeFileSync(markerPath, JSON.stringify({ pid: process.pid, token, t: Date.now() }), { mode: 384 });
2810
2819
  return { lockDir, markerPath, token };
2811
2820
  } catch (e) {
2812
2821
  if (e.code !== "EEXIST") throw e;
@@ -2900,7 +2909,7 @@ var require_cache = __commonJS({
2900
2909
  function writeCache(file, data) {
2901
2910
  mkdirSync(path.dirname(file), { recursive: true });
2902
2911
  const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
2903
- writeFileSync(tmp, JSON.stringify(data));
2912
+ writeFileSync(tmp, JSON.stringify(data), { mode: 384 });
2904
2913
  renameSync(tmp, file);
2905
2914
  }
2906
2915
  var _inflight = /* @__PURE__ */ new Map();
@@ -3038,6 +3047,12 @@ var require_glob = __commonJS({
3038
3047
  for (const c of res) if (c.re.test(rel)) return true;
3039
3048
  return false;
3040
3049
  }
3050
+ function pushFile(rel, abs, size) {
3051
+ files.push({ rel, abs, size });
3052
+ totalBytes += size;
3053
+ if (files.length > maxFiles) throw new Error(`directory expansion exceeded maxFiles=${maxFiles}. Narrow include or raise the limit.`);
3054
+ if (totalBytes > maxBytes) throw new Error(`directory expansion exceeded maxBytes=${maxBytes} bytes. Narrow include or raise the limit.`);
3055
+ }
3041
3056
  function descend(absDir, relDir) {
3042
3057
  let entries;
3043
3058
  try {
@@ -3069,8 +3084,7 @@ var require_glob = __commonJS({
3069
3084
  if (!st.isFile()) continue;
3070
3085
  if (matches(excludeRes, relPosix)) continue;
3071
3086
  if (!matches(includeRes, relPosix)) continue;
3072
- files.push({ rel: relPosix, abs: realTarget, size: st.size });
3073
- totalBytes += st.size;
3087
+ pushFile(relPosix, realTarget, st.size);
3074
3088
  } else if (ent.isDirectory()) {
3075
3089
  if (matches(excludeRes, relPosix) || matches(excludeRes, relPosix + "/**")) continue;
3076
3090
  descend(absChild, relPosix);
@@ -3083,18 +3097,11 @@ var require_glob = __commonJS({
3083
3097
  } catch (_) {
3084
3098
  continue;
3085
3099
  }
3086
- files.push({ rel: relPosix, abs: absChild, size: st.size });
3087
- totalBytes += st.size;
3100
+ pushFile(relPosix, absChild, st.size);
3088
3101
  }
3089
3102
  }
3090
3103
  }
3091
3104
  descend(rootAbs, "");
3092
- if (files.length > maxFiles) {
3093
- throw new Error(`directory expansion selected ${files.length} files; exceeds maxFiles=${maxFiles}. Narrow include or raise the limit.`);
3094
- }
3095
- if (totalBytes > maxBytes) {
3096
- throw new Error(`directory expansion selected ${totalBytes} bytes; exceeds maxBytes=${maxBytes}. Narrow include or raise the limit.`);
3097
- }
3098
3105
  files.sort((a, b) => a.rel.localeCompare(b.rel, "en"));
3099
3106
  return { files, totalBytes };
3100
3107
  }
@@ -3409,7 +3416,8 @@ ${ref.inline_text}` });
3409
3416
  res = await f(`${base}/files`, {
3410
3417
  method: "POST",
3411
3418
  headers: { "Authorization": `Bearer ${apiKey}` },
3412
- body: form
3419
+ body: form,
3420
+ redirect: "error"
3413
3421
  });
3414
3422
  } catch (err) {
3415
3423
  const e = new Error(`File upload network error: ${err && err.message || err}`);
@@ -3599,7 +3607,8 @@ ${ref.inline_text}` });
3599
3607
  "Authorization": `Bearer ${apiKey}`
3600
3608
  },
3601
3609
  body: JSON.stringify(payload),
3602
- signal: controller.signal
3610
+ signal: controller.signal,
3611
+ redirect: "error"
3603
3612
  });
3604
3613
  } catch (err) {
3605
3614
  const name = err && err.name;
@@ -4326,7 +4335,7 @@ var require_openrouter = __commonJS({
4326
4335
  const timer = setTimeout(() => controller.abort(), t);
4327
4336
  let res;
4328
4337
  try {
4329
- res = await f(url, { method: "POST", headers, body: JSON.stringify(payload), signal: controller.signal });
4338
+ res = await f(url, { method: "POST", headers, body: JSON.stringify(payload), signal: controller.signal, redirect: "error" });
4330
4339
  } catch (err) {
4331
4340
  const msg = String(err && err.message || err);
4332
4341
  if (err && err.name === "AbortError" || /abort/i.test(msg)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antonbabenko/deliberation-mcp",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
4
4
  "description": "Deliberation for Claude Code and any MCP host - GPT, Gemini, Grok, and OpenRouter expert subagents.",
5
5
  "mcpName": "io.github.antonbabenko/deliberation",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/antonbabenko/deliberation.git", "directory": "server/mcp" },