@mrrlin-dev/mcp 0.3.14 → 0.3.15
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/bin.cjs +327 -36
- package/package.json +2 -2
package/dist/bin.cjs
CHANGED
|
@@ -20799,9 +20799,8 @@ function renderBlock(block) {
|
|
|
20799
20799
|
lines2.push("");
|
|
20800
20800
|
return lines2.join("\n");
|
|
20801
20801
|
}
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
const candidate = actual;
|
|
20802
|
+
var KNOWN_MRRLIN_ENV_KEYS = /* @__PURE__ */ new Set(["MRRLIN_STAGING", "MRRLIN_API_BASE_URL", "MRRLIN_API_ALLOW_REMOTE"]);
|
|
20803
|
+
function commandArgsTimeoutEqual(candidate, expected) {
|
|
20805
20804
|
if (candidate.command !== expected.command) return false;
|
|
20806
20805
|
if (!Array.isArray(candidate.args)) return false;
|
|
20807
20806
|
if (candidate.args.length !== expected.args.length) return false;
|
|
@@ -20811,6 +20810,12 @@ function tablesEqual(actual, expected) {
|
|
|
20811
20810
|
if (candidate.startup_timeout_sec !== void 0 && candidate.startup_timeout_sec !== expected.startup_timeout_sec) {
|
|
20812
20811
|
return false;
|
|
20813
20812
|
}
|
|
20813
|
+
return true;
|
|
20814
|
+
}
|
|
20815
|
+
function tablesEqual(actual, expected) {
|
|
20816
|
+
if (typeof actual !== "object" || actual === null) return false;
|
|
20817
|
+
const candidate = actual;
|
|
20818
|
+
if (!commandArgsTimeoutEqual(candidate, expected)) return false;
|
|
20814
20819
|
const expectedEnv = expected.env ?? {};
|
|
20815
20820
|
const actualEnv = candidate.env && typeof candidate.env === "object" ? candidate.env : {};
|
|
20816
20821
|
const expectedKeys = Object.keys(expectedEnv);
|
|
@@ -20820,6 +20825,34 @@ function tablesEqual(actual, expected) {
|
|
|
20820
20825
|
}
|
|
20821
20826
|
return true;
|
|
20822
20827
|
}
|
|
20828
|
+
function isCoherentMrrlinEnv(env) {
|
|
20829
|
+
const keys = Object.keys(env);
|
|
20830
|
+
const staging = env.MRRLIN_STAGING;
|
|
20831
|
+
const url2 = env.MRRLIN_API_BASE_URL;
|
|
20832
|
+
const allowRemote = env.MRRLIN_API_ALLOW_REMOTE;
|
|
20833
|
+
const hasUrl = typeof url2 === "string" && url2.trim() !== "";
|
|
20834
|
+
if (keys.length === 1 && staging === "1") return true;
|
|
20835
|
+
if (keys.length === 1 && hasUrl) return true;
|
|
20836
|
+
if (keys.length === 2 && hasUrl && allowRemote === "1") return true;
|
|
20837
|
+
return false;
|
|
20838
|
+
}
|
|
20839
|
+
function classifyExistingBlock(actual, expected) {
|
|
20840
|
+
if (tablesEqual(actual, expected)) return "equal";
|
|
20841
|
+
if (typeof actual !== "object" || actual === null) return "conflict";
|
|
20842
|
+
const candidate = actual;
|
|
20843
|
+
if (expected.env && Object.keys(expected.env).length > 0) return "conflict";
|
|
20844
|
+
if (!commandArgsTimeoutEqual(candidate, expected)) return "conflict";
|
|
20845
|
+
const env = candidate.env && typeof candidate.env === "object" ? candidate.env : {};
|
|
20846
|
+
const keys = Object.keys(env);
|
|
20847
|
+
if (keys.length === 0) return "conflict";
|
|
20848
|
+
if (!keys.every((key) => KNOWN_MRRLIN_ENV_KEYS.has(key))) return "conflict";
|
|
20849
|
+
if (!isCoherentMrrlinEnv(env)) return "conflict";
|
|
20850
|
+
return "preserve-env-noop";
|
|
20851
|
+
}
|
|
20852
|
+
function renderEnvPreservedNotice(blockId, envKeys) {
|
|
20853
|
+
const keys = [...envKeys].sort().join(", ");
|
|
20854
|
+
return `[mrrlin-mcp install-codex] kept your existing [mcp_servers.${blockId}] env keys (${keys}) so your API target is preserved. Re-run with --force to replace or remove it.`;
|
|
20855
|
+
}
|
|
20823
20856
|
async function pathExists(target) {
|
|
20824
20857
|
try {
|
|
20825
20858
|
await import_promises.default.access(target);
|
|
@@ -20845,7 +20878,7 @@ async function installCodex(options) {
|
|
|
20845
20878
|
if (!await pathExists(configPath)) {
|
|
20846
20879
|
const content2 = blocks.map(renderBlock).join("").replace(/^\n/, "");
|
|
20847
20880
|
await import_promises.default.writeFile(configPath, content2, { mode: 384 });
|
|
20848
|
-
return { action: "created", configPath, prompts: await writePrompts() };
|
|
20881
|
+
return { action: "created", configPath, prompts: await writePrompts(), preservedEnvBlocks: [] };
|
|
20849
20882
|
}
|
|
20850
20883
|
const realPath = await resolveRealConfigPath(configPath);
|
|
20851
20884
|
let content = await import_promises.default.readFile(realPath, "utf8");
|
|
@@ -20862,10 +20895,19 @@ async function installCodex(options) {
|
|
|
20862
20895
|
throw Object.assign(new Error(err.detail), err);
|
|
20863
20896
|
}
|
|
20864
20897
|
let action = "noop";
|
|
20898
|
+
let mutated = false;
|
|
20899
|
+
const preservedEnvBlocks = [];
|
|
20865
20900
|
for (const block of blocks) {
|
|
20866
20901
|
const existingTable = parsed?.mcp_servers?.[block.id];
|
|
20867
20902
|
if (existingTable !== void 0) {
|
|
20868
|
-
|
|
20903
|
+
const classification = classifyExistingBlock(existingTable, block.expected);
|
|
20904
|
+
if (classification === "equal") continue;
|
|
20905
|
+
if (classification === "preserve-env-noop") {
|
|
20906
|
+
const env = existingTable.env ?? {};
|
|
20907
|
+
preservedEnvBlocks.push({ blockId: block.id, envKeys: Object.keys(env).sort() });
|
|
20908
|
+
if (action === "noop") action = "env-preserved";
|
|
20909
|
+
continue;
|
|
20910
|
+
}
|
|
20869
20911
|
if (!options.force) {
|
|
20870
20912
|
const err = {
|
|
20871
20913
|
code: "CODEX_CONFIG_CONFLICT",
|
|
@@ -20876,15 +20918,17 @@ async function installCodex(options) {
|
|
|
20876
20918
|
}
|
|
20877
20919
|
content = replaceBlock(content, block.blockRe, renderBlock(block));
|
|
20878
20920
|
action = "replaced";
|
|
20921
|
+
mutated = true;
|
|
20879
20922
|
} else {
|
|
20880
20923
|
content = appendBlock(content, renderBlock(block));
|
|
20881
20924
|
if (action !== "replaced") action = "appended";
|
|
20925
|
+
mutated = true;
|
|
20882
20926
|
}
|
|
20883
20927
|
}
|
|
20884
|
-
if (
|
|
20928
|
+
if (mutated) {
|
|
20885
20929
|
await import_promises.default.writeFile(realPath, content, { mode: 384 });
|
|
20886
20930
|
}
|
|
20887
|
-
return { action, configPath: realPath, prompts: await writePrompts() };
|
|
20931
|
+
return { action, configPath: realPath, prompts: await writePrompts(), preservedEnvBlocks };
|
|
20888
20932
|
}
|
|
20889
20933
|
async function installPrompts(codexHome, prompts, forcePrompts) {
|
|
20890
20934
|
if (prompts.length === 0) return [];
|
|
@@ -38564,7 +38608,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
|
|
|
38564
38608
|
}
|
|
38565
38609
|
|
|
38566
38610
|
// src/_generated/version.ts
|
|
38567
|
-
var PKG_VERSION = "0.3.
|
|
38611
|
+
var PKG_VERSION = "0.3.15";
|
|
38568
38612
|
|
|
38569
38613
|
// src/api-base-url.ts
|
|
38570
38614
|
var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
|
|
@@ -38778,10 +38822,42 @@ function withTimeout(promise2, ms, message) {
|
|
|
38778
38822
|
});
|
|
38779
38823
|
}
|
|
38780
38824
|
async function awaitTurnWithDeadline(opts) {
|
|
38825
|
+
const hasIdle = opts.idleTimeoutMs != null && opts.idleTimeoutMessage != null;
|
|
38826
|
+
const overallDeadlineAt = Date.now() + opts.timeoutMs;
|
|
38827
|
+
let overallTimer = null;
|
|
38828
|
+
let idleTimer = null;
|
|
38829
|
+
let settled = false;
|
|
38830
|
+
const clearTimers = () => {
|
|
38831
|
+
settled = true;
|
|
38832
|
+
if (overallTimer) clearTimeout(overallTimer);
|
|
38833
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
38834
|
+
overallTimer = null;
|
|
38835
|
+
idleTimer = null;
|
|
38836
|
+
};
|
|
38837
|
+
const activity = { touch: () => {
|
|
38838
|
+
} };
|
|
38839
|
+
const idleTimeoutMs = opts.idleTimeoutMs;
|
|
38840
|
+
const idleTimeoutMessage = opts.idleTimeoutMessage;
|
|
38841
|
+
const deadline = new Promise((_, reject) => {
|
|
38842
|
+
overallTimer = setTimeout(() => reject(new Error(opts.timeoutMessage)), opts.timeoutMs);
|
|
38843
|
+
if (idleTimeoutMs != null && idleTimeoutMessage != null) {
|
|
38844
|
+
const armIdle = () => {
|
|
38845
|
+
if (settled) return;
|
|
38846
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
38847
|
+
idleTimer = setTimeout(() => {
|
|
38848
|
+
reject(new Error(Date.now() >= overallDeadlineAt ? opts.timeoutMessage : idleTimeoutMessage));
|
|
38849
|
+
}, idleTimeoutMs);
|
|
38850
|
+
};
|
|
38851
|
+
activity.touch = armIdle;
|
|
38852
|
+
armIdle();
|
|
38853
|
+
}
|
|
38854
|
+
});
|
|
38781
38855
|
try {
|
|
38782
|
-
await
|
|
38856
|
+
await Promise.race([opts.run(activity), deadline]);
|
|
38783
38857
|
} catch (error51) {
|
|
38784
|
-
|
|
38858
|
+
clearTimers();
|
|
38859
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
38860
|
+
const isDeadline = message === opts.timeoutMessage || hasIdle && message === opts.idleTimeoutMessage;
|
|
38785
38861
|
if (isDeadline) {
|
|
38786
38862
|
try {
|
|
38787
38863
|
await withTimeout(
|
|
@@ -38795,6 +38871,7 @@ async function awaitTurnWithDeadline(opts) {
|
|
|
38795
38871
|
}
|
|
38796
38872
|
throw error51;
|
|
38797
38873
|
}
|
|
38874
|
+
clearTimers();
|
|
38798
38875
|
}
|
|
38799
38876
|
|
|
38800
38877
|
// src/worktree.ts
|
|
@@ -38987,9 +39064,12 @@ ${lines2.join("\n")}`;
|
|
|
38987
39064
|
var DEFAULT_STALE_SECONDS = 300;
|
|
38988
39065
|
var DEFAULT_TOUCH_INTERVAL_MS = 6e4;
|
|
38989
39066
|
var DEFAULT_CLAIM_INTERVAL_SECONDS = 30;
|
|
38990
|
-
var
|
|
39067
|
+
var DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS = 600;
|
|
39068
|
+
var DEFAULT_EXEC_RUN_MAX_SECONDS = 3600;
|
|
38991
39069
|
var DEFAULT_CONTEXT_MAX_ARTIFACTS = 50;
|
|
38992
39070
|
var EXECUTION_TURN_TIMEOUT_PREFIX = "Execution turn did not complete within";
|
|
39071
|
+
var EXECUTION_TURN_STALL_PREFIX = "Execution turn stalled: no Codex activity within";
|
|
39072
|
+
var EXECUTION_TURN_BUDGET_PREFIX = "Execution turn exceeded the run wall-clock budget";
|
|
38993
39073
|
var EXECUTION_TURN_TIMEOUT_RETRY_CODE = "execution_turn_timeout_retry";
|
|
38994
39074
|
var EXECUTION_TURN_TIMEOUT_RETRY_SUMMARY = "Execution turn timed out; requeued for one automatic retry.";
|
|
38995
39075
|
var EXECUTION_TURN_TIMEOUT_RETRY_CURSOR = "execution_turn_timeout_retry:1";
|
|
@@ -38999,14 +39079,26 @@ function readPositiveIntEnv(name, fallback) {
|
|
|
38999
39079
|
const parsed = Number.parseInt(raw, 10);
|
|
39000
39080
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
39001
39081
|
}
|
|
39002
|
-
function
|
|
39003
|
-
return readPositiveIntEnv("
|
|
39082
|
+
function execTurnIdleTimeoutMs() {
|
|
39083
|
+
return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_IDLE_TIMEOUT_SECONDS", DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS) * 1e3;
|
|
39084
|
+
}
|
|
39085
|
+
function execRunMaxMs() {
|
|
39086
|
+
return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_RUN_MAX_SECONDS", DEFAULT_EXEC_RUN_MAX_SECONDS) * 1e3;
|
|
39087
|
+
}
|
|
39088
|
+
function explicitLegacyTurnTimeoutMs() {
|
|
39089
|
+
const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS ?? "").trim();
|
|
39090
|
+
if (!raw) return null;
|
|
39091
|
+
const parsed = Number.parseInt(raw, 10);
|
|
39092
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed * 1e3 : null;
|
|
39093
|
+
}
|
|
39094
|
+
function executionTurnStallMessage(idleMs) {
|
|
39095
|
+
return `${EXECUTION_TURN_STALL_PREFIX} ${idleMs}ms.`;
|
|
39004
39096
|
}
|
|
39005
|
-
function
|
|
39006
|
-
return `${
|
|
39097
|
+
function executionTurnBudgetMessage(budgetMs) {
|
|
39098
|
+
return `${EXECUTION_TURN_BUDGET_PREFIX} (${budgetMs}ms).`;
|
|
39007
39099
|
}
|
|
39008
39100
|
function isExecutionTurnTimeoutReason(reason) {
|
|
39009
|
-
return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX);
|
|
39101
|
+
return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX) || reason.startsWith(EXECUTION_TURN_STALL_PREFIX);
|
|
39010
39102
|
}
|
|
39011
39103
|
function contextMaxArtifacts() {
|
|
39012
39104
|
return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS", DEFAULT_CONTEXT_MAX_ARTIFACTS);
|
|
@@ -39159,11 +39251,15 @@ async function runHasLease(deps, projectSlug, runId, leaseId) {
|
|
|
39159
39251
|
return false;
|
|
39160
39252
|
}
|
|
39161
39253
|
}
|
|
39162
|
-
async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inputText) {
|
|
39254
|
+
async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inputText, overallMs, phase) {
|
|
39163
39255
|
const mapper = new CodexBridgeEventMapper();
|
|
39164
39256
|
mapper.reset();
|
|
39165
39257
|
let turnError = null;
|
|
39166
39258
|
let interruptFailed = false;
|
|
39259
|
+
let touch = () => {
|
|
39260
|
+
};
|
|
39261
|
+
const turnStartedAt = Date.now();
|
|
39262
|
+
let lastActivityAt = turnStartedAt;
|
|
39167
39263
|
const persist = async (event) => {
|
|
39168
39264
|
if (event.type === "error") turnError = event.content;
|
|
39169
39265
|
const kind = artifactKindForEvent(event);
|
|
@@ -39185,6 +39281,11 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39185
39281
|
});
|
|
39186
39282
|
let acceptedTurnId = null;
|
|
39187
39283
|
const unsub = codex.onNotification((method, params) => {
|
|
39284
|
+
const notifThreadId = params?.threadId;
|
|
39285
|
+
if (!notifThreadId || notifThreadId === threadId) {
|
|
39286
|
+
lastActivityAt = Date.now();
|
|
39287
|
+
touch();
|
|
39288
|
+
}
|
|
39188
39289
|
const events = mapper.mapNotification(
|
|
39189
39290
|
{ method, params },
|
|
39190
39291
|
threadId
|
|
@@ -39192,9 +39293,8 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39192
39293
|
for (const event of events) pending.push(persist(event));
|
|
39193
39294
|
if (method === "turn/completed") {
|
|
39194
39295
|
const p = params;
|
|
39195
|
-
const notifThreadId = p?.threadId;
|
|
39196
39296
|
const notifTurnId = p?.turn?.id;
|
|
39197
|
-
const threadMatches = !
|
|
39297
|
+
const threadMatches = !p?.threadId || p.threadId === threadId;
|
|
39198
39298
|
const turnMatches = !acceptedTurnId || !notifTurnId || notifTurnId === acceptedTurnId;
|
|
39199
39299
|
if (threadMatches && turnMatches) resolveTurnEnded?.();
|
|
39200
39300
|
}
|
|
@@ -39204,10 +39304,11 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39204
39304
|
});
|
|
39205
39305
|
}, deps.touchIntervalMs);
|
|
39206
39306
|
touchTimer.unref?.();
|
|
39307
|
+
const idleMs = execTurnIdleTimeoutMs();
|
|
39207
39308
|
try {
|
|
39208
|
-
const timeoutMs = execTurnTimeoutMs();
|
|
39209
39309
|
await awaitTurnWithDeadline({
|
|
39210
|
-
run: async () => {
|
|
39310
|
+
run: async (activity) => {
|
|
39311
|
+
touch = activity.touch;
|
|
39211
39312
|
const accepted = await codex.turn.start({
|
|
39212
39313
|
threadId,
|
|
39213
39314
|
input: [{ type: "text", text: inputText }]
|
|
@@ -39216,8 +39317,10 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39216
39317
|
await turnEnded;
|
|
39217
39318
|
},
|
|
39218
39319
|
interrupt: () => codex.turn.interrupt({ threadId }),
|
|
39219
|
-
timeoutMs,
|
|
39220
|
-
timeoutMessage:
|
|
39320
|
+
timeoutMs: overallMs,
|
|
39321
|
+
timeoutMessage: executionTurnBudgetMessage(overallMs),
|
|
39322
|
+
idleTimeoutMs: idleMs,
|
|
39323
|
+
idleTimeoutMessage: executionTurnStallMessage(idleMs),
|
|
39221
39324
|
onInterruptFailed: () => {
|
|
39222
39325
|
interruptFailed = true;
|
|
39223
39326
|
}
|
|
@@ -39226,6 +39329,19 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39226
39329
|
await Promise.all(pending);
|
|
39227
39330
|
} catch (error51) {
|
|
39228
39331
|
turnError = error51 instanceof Error ? error51.message : String(error51);
|
|
39332
|
+
if (turnError.startsWith(EXECUTION_TURN_STALL_PREFIX) || turnError.startsWith(EXECUTION_TURN_BUDGET_PREFIX)) {
|
|
39333
|
+
console.warn(
|
|
39334
|
+
JSON.stringify({
|
|
39335
|
+
event: "execution_turn_deadline",
|
|
39336
|
+
projectSlug,
|
|
39337
|
+
runId,
|
|
39338
|
+
phase,
|
|
39339
|
+
kind: turnError.startsWith(EXECUTION_TURN_BUDGET_PREFIX) ? "budget" : "stall",
|
|
39340
|
+
elapsedMs: Date.now() - turnStartedAt,
|
|
39341
|
+
sinceLastActivityMs: Date.now() - lastActivityAt
|
|
39342
|
+
})
|
|
39343
|
+
);
|
|
39344
|
+
}
|
|
39229
39345
|
} finally {
|
|
39230
39346
|
clearInterval(touchTimer);
|
|
39231
39347
|
unsub();
|
|
@@ -39247,6 +39363,12 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39247
39363
|
return;
|
|
39248
39364
|
}
|
|
39249
39365
|
const leaseId = claimed.leaseId;
|
|
39366
|
+
const attemptDeadlineMs = Date.now() + execRunMaxMs();
|
|
39367
|
+
const remainingTurnBudgetMs = () => {
|
|
39368
|
+
const remaining = attemptDeadlineMs - Date.now();
|
|
39369
|
+
const legacy = explicitLegacyTurnTimeoutMs();
|
|
39370
|
+
return legacy != null ? Math.min(legacy, remaining) : remaining;
|
|
39371
|
+
};
|
|
39250
39372
|
const task = await deps.client.getTask(projectSlug, claimed.taskId);
|
|
39251
39373
|
const compactEnabled = (process.env.MRRLIN_MCP_COMPACT_DISABLE ?? "").trim() !== "1";
|
|
39252
39374
|
const context = await gatherContext(deps.client, projectSlug, task, runId);
|
|
@@ -39284,6 +39406,7 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39284
39406
|
runId,
|
|
39285
39407
|
defaultBranch: await getDefaultBranch(projectSlug)
|
|
39286
39408
|
});
|
|
39409
|
+
if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
|
|
39287
39410
|
await deps.client.updateExecutionRun(projectSlug, runId, {
|
|
39288
39411
|
worktreeId: worktreePath,
|
|
39289
39412
|
branch: worktreeBranch(runId)
|
|
@@ -39334,11 +39457,17 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39334
39457
|
ephemeral: null
|
|
39335
39458
|
},
|
|
39336
39459
|
onThreadIdChanged: async (newId) => {
|
|
39460
|
+
if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
|
|
39337
39461
|
await deps.client.updateTask(projectSlug, task.id, { directorThreadId: newId });
|
|
39338
39462
|
}
|
|
39339
39463
|
});
|
|
39340
39464
|
if (!resumed) {
|
|
39341
|
-
const
|
|
39465
|
+
const primingBudgetMs = remainingTurnBudgetMs();
|
|
39466
|
+
if (primingBudgetMs <= 0) {
|
|
39467
|
+
await failRun(deps, projectSlug, task, runId, leaseId, executionTurnBudgetMessage(execRunMaxMs()));
|
|
39468
|
+
return;
|
|
39469
|
+
}
|
|
39470
|
+
const primed = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming");
|
|
39342
39471
|
if (!primed.ok) {
|
|
39343
39472
|
await resetWedgedThread(deps, projectSlug, task.id, primed.interruptFailed);
|
|
39344
39473
|
await failRun(deps, projectSlug, task, runId, leaseId, primed.error);
|
|
@@ -39348,7 +39477,12 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39348
39477
|
const executionPrompt = resumed ? `${bundle}
|
|
39349
39478
|
|
|
39350
39479
|
Continue executing task ${task.id}. Make the next concrete increment of progress and record artifacts/judgements via MCP tools.` : `Continue executing task ${task.id}. Make the next concrete increment of progress and record artifacts/judgements via MCP tools.`;
|
|
39351
|
-
const
|
|
39480
|
+
const workBudgetMs = remainingTurnBudgetMs();
|
|
39481
|
+
if (workBudgetMs <= 0) {
|
|
39482
|
+
await failRun(deps, projectSlug, task, runId, leaseId, executionTurnBudgetMessage(execRunMaxMs()));
|
|
39483
|
+
return;
|
|
39484
|
+
}
|
|
39485
|
+
const outcome = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work");
|
|
39352
39486
|
if (!outcome.ok) {
|
|
39353
39487
|
await resetWedgedThread(deps, projectSlug, task.id, outcome.interruptFailed);
|
|
39354
39488
|
await failRun(deps, projectSlug, task, runId, leaseId, outcome.error);
|
|
@@ -44145,6 +44279,9 @@ function buildThreadStartParams(msg, config2, desiredModel) {
|
|
|
44145
44279
|
"request behind the task (pure system bookkeeping) \u2014 note that omitting it makes the gate fail-closed.",
|
|
44146
44280
|
"",
|
|
44147
44281
|
"Spec authoring contract \u2014 when creating or updating a `Specs/{id}` wiki page:",
|
|
44282
|
+
"- BEGIN the page with YAML frontmatter naming the gated task: a first line `---`, then",
|
|
44283
|
+
" `task: {taskId}` (the task this spec gates, e.g. `task: GT-1023`), then `---`. Never change it",
|
|
44284
|
+
" to another task id \u2014 it is the machine-readable identity the consensus gate validates against.",
|
|
44148
44285
|
"- In `## Specification`, write each requirement as `### Requirement: <name>` followed by one",
|
|
44149
44286
|
" SHALL sentence stating the externally visible behavior.",
|
|
44150
44287
|
"- Give every requirement at least one `#### Scenario: <name>` with `- WHEN ...` / `- THEN ...`",
|
|
@@ -49624,6 +49761,71 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
|
|
|
49624
49761
|
return { outcome: "CONSENSUS_BLOCKED", rounds: input.rounds, finalArtifact: artifact, history };
|
|
49625
49762
|
}
|
|
49626
49763
|
|
|
49764
|
+
// src/consensus/spec-identity.ts
|
|
49765
|
+
function validateSpecIdentity(markdown, taskId) {
|
|
49766
|
+
const target = taskId.toUpperCase();
|
|
49767
|
+
const fieldToken = frontmatterTaskToken(markdown);
|
|
49768
|
+
if (fieldToken !== null && fieldToken.toUpperCase() !== target) return false;
|
|
49769
|
+
const headingToken = firstHeadingToken(markdown);
|
|
49770
|
+
if (headingToken !== null && headingToken.toUpperCase() !== target) return false;
|
|
49771
|
+
return true;
|
|
49772
|
+
}
|
|
49773
|
+
var TASK_ID_TOKEN = /\b[A-Za-z]{1,12}-\d{1,10}\b/;
|
|
49774
|
+
var ATX_HEADING = /^ {0,3}#{1,6}\s+(.*)$/;
|
|
49775
|
+
var FRONTMATTER_TASK = /^task(?:_id)?:\s*(.+)$/i;
|
|
49776
|
+
function frontmatterTaskToken(markdown) {
|
|
49777
|
+
const lines2 = markdown.split("\n");
|
|
49778
|
+
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
49779
|
+
if (firstContent === -1 || lines2[firstContent]?.trim() !== "---") return null;
|
|
49780
|
+
for (let i = firstContent + 1; i < lines2.length; i++) {
|
|
49781
|
+
const trimmed = (lines2[i] ?? "").trim();
|
|
49782
|
+
if (trimmed === "---") break;
|
|
49783
|
+
const m = FRONTMATTER_TASK.exec(trimmed);
|
|
49784
|
+
if (m) return firstTaskIdToken(m[1] ?? "");
|
|
49785
|
+
}
|
|
49786
|
+
return null;
|
|
49787
|
+
}
|
|
49788
|
+
function firstHeadingToken(markdown) {
|
|
49789
|
+
const heading = firstHeading(markdown);
|
|
49790
|
+
return heading === null ? null : firstTaskIdToken(heading);
|
|
49791
|
+
}
|
|
49792
|
+
function firstHeading(markdown) {
|
|
49793
|
+
const lines2 = markdown.split("\n");
|
|
49794
|
+
let i = 0;
|
|
49795
|
+
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
49796
|
+
if (firstContent !== -1 && lines2[firstContent]?.trim() === "---") {
|
|
49797
|
+
i = firstContent + 1;
|
|
49798
|
+
while (i < lines2.length && lines2[i]?.trim() !== "---") i++;
|
|
49799
|
+
i++;
|
|
49800
|
+
}
|
|
49801
|
+
let inFence = false;
|
|
49802
|
+
let inComment = false;
|
|
49803
|
+
for (; i < lines2.length; i++) {
|
|
49804
|
+
const line = lines2[i] ?? "";
|
|
49805
|
+
const trimmed = line.trim();
|
|
49806
|
+
if (inComment) {
|
|
49807
|
+
if (trimmed.includes("-->")) inComment = false;
|
|
49808
|
+
continue;
|
|
49809
|
+
}
|
|
49810
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
49811
|
+
inFence = !inFence;
|
|
49812
|
+
continue;
|
|
49813
|
+
}
|
|
49814
|
+
if (inFence) continue;
|
|
49815
|
+
if (trimmed.startsWith("<!--")) {
|
|
49816
|
+
if (!trimmed.includes("-->")) inComment = true;
|
|
49817
|
+
continue;
|
|
49818
|
+
}
|
|
49819
|
+
const m = ATX_HEADING.exec(line);
|
|
49820
|
+
if (m) return m[1] ?? "";
|
|
49821
|
+
}
|
|
49822
|
+
return null;
|
|
49823
|
+
}
|
|
49824
|
+
function firstTaskIdToken(text) {
|
|
49825
|
+
const m = TASK_ID_TOKEN.exec(text);
|
|
49826
|
+
return m ? m[0] : null;
|
|
49827
|
+
}
|
|
49828
|
+
|
|
49627
49829
|
// src/consensus/spec-gate.ts
|
|
49628
49830
|
var SPEC_HEADS = ["plan-reviewer", "architect", "security-analyst", "scope-analyst"];
|
|
49629
49831
|
async function runSpecGate(_target, deps, opts) {
|
|
@@ -49645,6 +49847,7 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49645
49847
|
const heads = [...SPEC_HEADS, ...extraHeads.filter((h) => !SPEC_HEADS.includes(h))];
|
|
49646
49848
|
const spec = await deps.readSpec();
|
|
49647
49849
|
let currentHash = spec.contentHash;
|
|
49850
|
+
let currentMarkdown = spec.markdownBody;
|
|
49648
49851
|
let conflicted = false;
|
|
49649
49852
|
const result = await runConsensus({
|
|
49650
49853
|
artifact: spec.markdownBody,
|
|
@@ -49656,12 +49859,20 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49656
49859
|
revise: async (artifact, accepted) => {
|
|
49657
49860
|
const revised = await deps.runReviser(artifact, accepted);
|
|
49658
49861
|
if (!revised.ok) return artifact;
|
|
49862
|
+
if (intent.taskId !== null && !validateSpecIdentity(revised.text, intent.taskId)) {
|
|
49863
|
+
process.stderr.write(
|
|
49864
|
+
`[spec-gate] discarding drifted revision for ${spec.pageId}: heading does not identify ${intent.taskId}
|
|
49865
|
+
`
|
|
49866
|
+
);
|
|
49867
|
+
return artifact;
|
|
49868
|
+
}
|
|
49659
49869
|
const cas = await deps.casUpdateSpec(revised.text, currentHash);
|
|
49660
49870
|
if (!cas.ok) {
|
|
49661
49871
|
conflicted = true;
|
|
49662
49872
|
throw new Error("SPEC_CHANGED_DURING_REVIEW");
|
|
49663
49873
|
}
|
|
49664
49874
|
currentHash = cas.contentHash;
|
|
49875
|
+
currentMarkdown = revised.text;
|
|
49665
49876
|
return revised.text;
|
|
49666
49877
|
}
|
|
49667
49878
|
// Only OUR conflict throw is swallowed (→ null). Anything else is a real
|
|
@@ -49671,6 +49882,22 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49671
49882
|
await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
|
|
49672
49883
|
return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
|
|
49673
49884
|
}
|
|
49885
|
+
if (result.outcome === "CONVERGED" && intent.taskId !== null && !validateSpecIdentity(currentMarkdown, intent.taskId)) {
|
|
49886
|
+
process.stderr.write(
|
|
49887
|
+
`[spec-gate] blocking promotion for ${spec.pageId}: converged spec heading does not identify ${intent.taskId}
|
|
49888
|
+
`
|
|
49889
|
+
);
|
|
49890
|
+
return {
|
|
49891
|
+
outcome: "CONSENSUS_BLOCKED",
|
|
49892
|
+
clarifications: [
|
|
49893
|
+
{
|
|
49894
|
+
question: `SPEC_IDENTITY_MISMATCH: this spec page gates ${intent.taskId}, but its heading identifies a different task \u2014 rewrite the spec heading and content for ${intent.taskId}, or re-link the task.`,
|
|
49895
|
+
whyBlocking: "converged spec content belongs to a different task than the one it gates",
|
|
49896
|
+
category: "scope"
|
|
49897
|
+
}
|
|
49898
|
+
]
|
|
49899
|
+
};
|
|
49900
|
+
}
|
|
49674
49901
|
await deps.writeEvidence({
|
|
49675
49902
|
specPageId: spec.pageId,
|
|
49676
49903
|
outcome: result.outcome,
|
|
@@ -49888,6 +50115,21 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
|
49888
50115
|
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
49889
50116
|
var import_node_url2 = require("node:url");
|
|
49890
50117
|
|
|
50118
|
+
// src/consensus/resolve-gated-task.ts
|
|
50119
|
+
async function resolveGatedTaskId(client, projectSlug, page) {
|
|
50120
|
+
const tasks = await client.listTasks(projectSlug);
|
|
50121
|
+
const linking = tasks.find((t) => t.specWikiPageId === page.id);
|
|
50122
|
+
if (linking) {
|
|
50123
|
+
if (page.taskId !== linking.id) {
|
|
50124
|
+
throw new Error(
|
|
50125
|
+
`spec page ${page.id} is linked by task ${linking.id} (tasks.spec_wiki_page_id) but its wiki_pages.task_id back-pointer is ${page.taskId === null ? "null" : page.taskId} \u2014 refusing to gate with a drifted identity`
|
|
50126
|
+
);
|
|
50127
|
+
}
|
|
50128
|
+
return linking.id;
|
|
50129
|
+
}
|
|
50130
|
+
return page.taskId;
|
|
50131
|
+
}
|
|
50132
|
+
|
|
49891
50133
|
// src/consensus/code-gate-git.ts
|
|
49892
50134
|
var import_node_child_process6 = require("node:child_process");
|
|
49893
50135
|
var DIFF_MAX_BYTES = 2e5;
|
|
@@ -50132,6 +50374,23 @@ function loadPersona(name) {
|
|
|
50132
50374
|
personaCache.set(name, content);
|
|
50133
50375
|
return content;
|
|
50134
50376
|
}
|
|
50377
|
+
function specHeadIntentBlock(taskId, title, seed) {
|
|
50378
|
+
const identity = taskId ? `--- TASK IDENTITY (the spec MUST be about THIS task; if its scope or direction contradicts this identity, raise a \`scope\` issue instead of steering the spec elsewhere) ---
|
|
50379
|
+
TASK ${taskId}: ${title ?? ""}
|
|
50380
|
+
|
|
50381
|
+
` : "";
|
|
50382
|
+
const intent = seed ? `--- OPERATOR INTENT (the operator's own words \u2014 judge the spec against THIS) ---
|
|
50383
|
+
${seed}
|
|
50384
|
+
|
|
50385
|
+
` : "";
|
|
50386
|
+
return `${identity}${intent}`;
|
|
50387
|
+
}
|
|
50388
|
+
function reviserIdentityInstructions(taskId, title) {
|
|
50389
|
+
const lock = taskId ? `This spec gates task ${taskId}${title ? ` ("${title}")` : ""}. You MUST NOT change the spec's task identity or heading, retarget it to a different task id, or invert its direction (e.g. live-submission vs preparation-only). Preserve the leading \`task: ${taskId}\` frontmatter EXACTLY; never change it to another task id. Address ONLY the accepted issues.
|
|
50390
|
+
|
|
50391
|
+
` : "";
|
|
50392
|
+
return `${lock}You revise a spec to address accepted issues. Return ONLY the full revised markdown, no commentary.`;
|
|
50393
|
+
}
|
|
50135
50394
|
function buildSpecGateDeps(args) {
|
|
50136
50395
|
const { client, projectSlug, specPageId, cwd, codexExecutable, extraHeadCount = 0 } = args;
|
|
50137
50396
|
const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 3);
|
|
@@ -50142,10 +50401,16 @@ function buildSpecGateDeps(args) {
|
|
|
50142
50401
|
if (!intentPromise) {
|
|
50143
50402
|
intentPromise = (async () => {
|
|
50144
50403
|
const page = await client.getWikiPage(projectSlug, specPageId);
|
|
50145
|
-
|
|
50146
|
-
|
|
50404
|
+
if (!page) {
|
|
50405
|
+
throw new Error(`spec page ${projectSlug}/${specPageId} not found \u2014 cannot gate`);
|
|
50406
|
+
}
|
|
50407
|
+
const taskId = await resolveGatedTaskId(client, projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
50408
|
+
if (taskId === null) return { taskId: null, seed: null, title: null };
|
|
50147
50409
|
const task = await client.getTask(projectSlug, taskId);
|
|
50148
|
-
|
|
50410
|
+
if (!task) {
|
|
50411
|
+
throw new Error(`gated task ${projectSlug}/${taskId} not found for spec ${specPageId} \u2014 cannot gate`);
|
|
50412
|
+
}
|
|
50413
|
+
return { taskId, seed: task.operatorSeedRequest ?? null, title: task.title ?? "" };
|
|
50149
50414
|
})();
|
|
50150
50415
|
}
|
|
50151
50416
|
return intentPromise;
|
|
@@ -50170,11 +50435,8 @@ function buildSpecGateDeps(args) {
|
|
|
50170
50435
|
},
|
|
50171
50436
|
async runHead(personaName, artifact, dismissed) {
|
|
50172
50437
|
const persona = loadPersona(personaName);
|
|
50173
|
-
const { seed } = await loadIntent();
|
|
50174
|
-
const intentBlock =
|
|
50175
|
-
${seed}
|
|
50176
|
-
|
|
50177
|
-
` : "";
|
|
50438
|
+
const { taskId, title, seed } = await loadIntent();
|
|
50439
|
+
const intentBlock = specHeadIntentBlock(taskId, title, seed);
|
|
50178
50440
|
return runCodexExec(
|
|
50179
50441
|
{
|
|
50180
50442
|
prompt: `Review this spec against the operator's intent.
|
|
@@ -50205,6 +50467,7 @@ ${artifact}`,
|
|
|
50205
50467
|
},
|
|
50206
50468
|
async runReviser(artifact, accepted) {
|
|
50207
50469
|
const acceptedList = accepted.map((a) => `- (${a.category}) ${a.text}`).join("\n");
|
|
50470
|
+
const { taskId, title } = await loadIntent();
|
|
50208
50471
|
return runCodexExec(
|
|
50209
50472
|
{
|
|
50210
50473
|
prompt: `Accepted issues:
|
|
@@ -50212,7 +50475,7 @@ ${acceptedList}
|
|
|
50212
50475
|
|
|
50213
50476
|
--- SPEC ---
|
|
50214
50477
|
${artifact}`,
|
|
50215
|
-
developerInstructions:
|
|
50478
|
+
developerInstructions: reviserIdentityInstructions(taskId, title),
|
|
50216
50479
|
cwd,
|
|
50217
50480
|
timeoutMs,
|
|
50218
50481
|
sandbox: "read-only",
|
|
@@ -50363,6 +50626,13 @@ async function cachedResult(client, input) {
|
|
|
50363
50626
|
if (!page) return null;
|
|
50364
50627
|
const review = await client.getSpecConsensusReview(input.projectSlug, input.pageId, page.contentHash);
|
|
50365
50628
|
if (review?.outcome === "CONVERGED") {
|
|
50629
|
+
const gatedTaskId = await resolveGatedTaskId(client, input.projectSlug, {
|
|
50630
|
+
id: page.id,
|
|
50631
|
+
taskId: page.taskId ?? null
|
|
50632
|
+
});
|
|
50633
|
+
if (gatedTaskId !== null && !validateSpecIdentity(page.markdownBody, gatedTaskId)) {
|
|
50634
|
+
return null;
|
|
50635
|
+
}
|
|
50366
50636
|
return { status: "already_converged", payload: { outcome: "CONVERGED", contentHash: page.contentHash } };
|
|
50367
50637
|
}
|
|
50368
50638
|
if (review?.outcome === "CONSENSUS_BLOCKED") {
|
|
@@ -52585,6 +52855,13 @@ Usage:
|
|
|
52585
52855
|
non-loopback URL (see \`mrrlin-mcp serve\` above).
|
|
52586
52856
|
URL and MRRLIN_STAGING are mutually exclusive at
|
|
52587
52857
|
bake time \u2014 URL wins, mirrors serve's resolver.
|
|
52858
|
+
Re-running BARE (no env vars) over a block you
|
|
52859
|
+
already pinned to staging/a remote target is safe:
|
|
52860
|
+
it keeps your env verbatim (reported as
|
|
52861
|
+
'env-preserved') instead of conflicting, so the
|
|
52862
|
+
same setup command doubles as an update command.
|
|
52863
|
+
To CHANGE or REMOVE the baked target, re-run with
|
|
52864
|
+
--force (or the new env vars).
|
|
52588
52865
|
--force replaces an existing conflicting block.
|
|
52589
52866
|
--force-prompts overwrites prompt files that
|
|
52590
52867
|
already exist with different content. WITHOUT it,
|
|
@@ -52631,8 +52908,18 @@ Usage:
|
|
|
52631
52908
|
- MRRLIN_DIRECTOR_BRIDGE_CODEX_IDLE_SECONDS (default 300;
|
|
52632
52909
|
how long the warm codex app-server is kept alive
|
|
52633
52910
|
between turns before it is released)
|
|
52634
|
-
-
|
|
52635
|
-
|
|
52911
|
+
- MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_IDLE_TIMEOUT_SECONDS (default 600;
|
|
52912
|
+
an execution turn is cut only after this long with NO codex
|
|
52913
|
+
signal at all \u2014 long-but-active turns keep running. A stall
|
|
52914
|
+
earns one automatic retry)
|
|
52915
|
+
- MRRLIN_DIRECTOR_BRIDGE_EXEC_RUN_MAX_SECONDS (default 3600;
|
|
52916
|
+
overall wall-clock budget for the active-turn portion of one
|
|
52917
|
+
claim attempt, measured from claim. Exceeding it hands off with
|
|
52918
|
+
NO retry. Setup/post-turn hangs are bounded by the stale reaper,
|
|
52919
|
+
not by this budget)
|
|
52920
|
+
- MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS (DEPRECATED;
|
|
52921
|
+
unset by default. When set, an extra per-turn hard cap: the
|
|
52922
|
+
effective turn deadline becomes min(this, remaining run budget))
|
|
52636
52923
|
- MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS (default 50;
|
|
52637
52924
|
most-recent artifacts kept in the execution context bundle)
|
|
52638
52925
|
|
|
@@ -52747,6 +53034,10 @@ async function main() {
|
|
|
52747
53034
|
});
|
|
52748
53035
|
process.stderr.write(`[mrrlin-mcp install-codex] ${result.action} ${result.configPath}
|
|
52749
53036
|
`);
|
|
53037
|
+
for (const preserved of result.preservedEnvBlocks) {
|
|
53038
|
+
process.stderr.write(`${renderEnvPreservedNotice(preserved.blockId, preserved.envKeys)}
|
|
53039
|
+
`);
|
|
53040
|
+
}
|
|
52750
53041
|
for (const p of result.prompts) {
|
|
52751
53042
|
process.stderr.write(`[mrrlin-mcp install-codex] prompt ${p.name}: ${p.action} ${p.promptPath}
|
|
52752
53043
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrlin-dev/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mrrlin-mcp": "dist/bin.cjs"
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@mrrlin/client": "0.0.0",
|
|
26
26
|
"@mrrlin/codex-client": "0.0.0",
|
|
27
27
|
"@mrrlin/director-e2e": "0.0.0",
|
|
28
|
-
"@mrrlin/schemas": "0.0.0",
|
|
29
28
|
"@mrrlin/tsconfig": "0.0.0",
|
|
29
|
+
"@mrrlin/schemas": "0.0.0",
|
|
30
30
|
"@mrrlin/wiki": "0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|