@mrrlin-dev/mcp 0.3.15 → 0.3.20
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
CHANGED
|
@@ -20985,10 +20985,15 @@ var import_node_child_process4 = require("node:child_process");
|
|
|
20985
20985
|
var import_node_crypto5 = __toESM(require("node:crypto"), 1);
|
|
20986
20986
|
|
|
20987
20987
|
// src/turn-replay-buffer.ts
|
|
20988
|
-
var DEFAULT_GRACE_MS = 3e4;
|
|
20989
20988
|
var DEFAULT_MAX_EVENTS = 2e3;
|
|
20990
20989
|
var DEFAULT_MAX_BYTES = 4e6;
|
|
20991
|
-
var
|
|
20990
|
+
var DEFAULT_ACK_LINGER_MS = 1e4;
|
|
20991
|
+
var DEFAULT_RESULT_RETENTION_MS = 36e5;
|
|
20992
|
+
function resultRetentionMs() {
|
|
20993
|
+
const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_RESULT_RETENTION_SECONDS ?? "").trim();
|
|
20994
|
+
const parsed = Number.parseInt(raw, 10);
|
|
20995
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed * 1e3 : DEFAULT_RESULT_RETENTION_MS;
|
|
20996
|
+
}
|
|
20992
20997
|
function createReplayBuffer(opts) {
|
|
20993
20998
|
return {
|
|
20994
20999
|
turnId: opts.turnId,
|
|
@@ -21000,14 +21005,15 @@ function createReplayBuffer(opts) {
|
|
|
21000
21005
|
terminalDelivered: false,
|
|
21001
21006
|
replaying: false,
|
|
21002
21007
|
outboundQueue: [],
|
|
21003
|
-
graceTimer: null,
|
|
21004
21008
|
finishedAt: null,
|
|
21009
|
+
finishedSuspendedMs: null,
|
|
21010
|
+
ackedAt: null,
|
|
21005
21011
|
bytesUsed: 0,
|
|
21006
|
-
graceMs: opts.graceMs ?? DEFAULT_GRACE_MS,
|
|
21007
21012
|
maxEvents: opts.maxEvents ?? DEFAULT_MAX_EVENTS,
|
|
21008
21013
|
maxBytes: opts.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
21009
|
-
|
|
21010
|
-
|
|
21014
|
+
ackLingerMs: opts.ackLingerMs ?? DEFAULT_ACK_LINGER_MS,
|
|
21015
|
+
retentionMs: opts.retentionMs ?? resultRetentionMs(),
|
|
21016
|
+
triggerDetach: null
|
|
21011
21017
|
};
|
|
21012
21018
|
}
|
|
21013
21019
|
function recordOutgoing(buf, msg) {
|
|
@@ -21016,9 +21022,10 @@ function recordOutgoing(buf, msg) {
|
|
|
21016
21022
|
buf.events.push(entry);
|
|
21017
21023
|
buf.bytesUsed += approxByteSize(msg);
|
|
21018
21024
|
while ((buf.events.length > buf.maxEvents || buf.bytesUsed > buf.maxBytes) && buf.events.length > 0) {
|
|
21019
|
-
const
|
|
21020
|
-
if (!
|
|
21021
|
-
buf.
|
|
21025
|
+
const head = buf.events[0];
|
|
21026
|
+
if (!head || isTerminalEvent(head.msg)) break;
|
|
21027
|
+
buf.events.shift();
|
|
21028
|
+
buf.bytesUsed -= approxByteSize(head.msg);
|
|
21022
21029
|
}
|
|
21023
21030
|
return entry;
|
|
21024
21031
|
}
|
|
@@ -21027,19 +21034,7 @@ function eventsAfter(buf, lastSeq) {
|
|
|
21027
21034
|
if (first && lastSeq < first.seq - 1) return null;
|
|
21028
21035
|
return buf.events.filter((e) => e.seq > lastSeq);
|
|
21029
21036
|
}
|
|
21030
|
-
function
|
|
21031
|
-
if (buf.graceTimer !== null) return;
|
|
21032
|
-
buf.state = "grace";
|
|
21033
|
-
buf.graceTimer = setTimeout(onExpire, buf.graceMs);
|
|
21034
|
-
}
|
|
21035
|
-
function cancelGrace(buf) {
|
|
21036
|
-
if (buf.graceTimer !== null) {
|
|
21037
|
-
clearTimeout(buf.graceTimer);
|
|
21038
|
-
buf.graceTimer = null;
|
|
21039
|
-
}
|
|
21040
|
-
if (buf.state === "grace") buf.state = "live";
|
|
21041
|
-
}
|
|
21042
|
-
function recordInterrupted(buf) {
|
|
21037
|
+
function recordInterrupted(buf, now = Date.now(), suspendedMsNow = 0) {
|
|
21043
21038
|
if (buf.terminalDelivered) return null;
|
|
21044
21039
|
const msg = {
|
|
21045
21040
|
type: "event",
|
|
@@ -21047,16 +21042,31 @@ function recordInterrupted(buf) {
|
|
|
21047
21042
|
};
|
|
21048
21043
|
const entry = recordOutgoing(buf, msg);
|
|
21049
21044
|
buf.terminalDelivered = true;
|
|
21045
|
+
if (buf.finishedAt === null) {
|
|
21046
|
+
buf.finishedAt = now;
|
|
21047
|
+
buf.finishedSuspendedMs = suspendedMsNow;
|
|
21048
|
+
}
|
|
21050
21049
|
return entry;
|
|
21051
21050
|
}
|
|
21052
|
-
function markCompleted(buf, now) {
|
|
21051
|
+
function markCompleted(buf, now, suspendedMsNow = 0) {
|
|
21053
21052
|
buf.state = "completed";
|
|
21054
21053
|
buf.finishedAt = now;
|
|
21054
|
+
buf.finishedSuspendedMs = suspendedMsNow;
|
|
21055
21055
|
}
|
|
21056
|
-
function
|
|
21057
|
-
if (buf.
|
|
21056
|
+
function recordAck(buf, turnId, lastSeq, now) {
|
|
21057
|
+
if (buf.turnId !== turnId) return false;
|
|
21058
|
+
if (lastSeq !== buf.lastSeq) return false;
|
|
21059
|
+
if (buf.state !== "completed" && buf.state !== "interrupted") return false;
|
|
21060
|
+
if (buf.ackedAt === null) buf.ackedAt = now;
|
|
21061
|
+
return true;
|
|
21062
|
+
}
|
|
21063
|
+
function shouldGc(buf, now, suspendedMsNow = 0) {
|
|
21064
|
+
if (buf.state !== "completed" && buf.state !== "interrupted") return false;
|
|
21058
21065
|
if (buf.finishedAt === null) return false;
|
|
21059
|
-
return now - buf.
|
|
21066
|
+
if (buf.ackedAt !== null) return now - buf.ackedAt > buf.ackLingerMs;
|
|
21067
|
+
const suspendedSinceFinish = suspendedMsNow - (buf.finishedSuspendedMs ?? suspendedMsNow);
|
|
21068
|
+
const awakeElapsed = now - buf.finishedAt - Math.max(0, suspendedSinceFinish);
|
|
21069
|
+
return awakeElapsed > buf.retentionMs;
|
|
21060
21070
|
}
|
|
21061
21071
|
function incrementBindEpoch(buf) {
|
|
21062
21072
|
return ++buf.bindEpoch;
|
|
@@ -21077,6 +21087,41 @@ function approxByteSize(msg) {
|
|
|
21077
21087
|
}
|
|
21078
21088
|
}
|
|
21079
21089
|
|
|
21090
|
+
// src/suspension-detector.ts
|
|
21091
|
+
var DEFAULT_SUSPEND_TICK_MS = 15e3;
|
|
21092
|
+
var DEFAULT_SUSPEND_GAP_MS = 45e3;
|
|
21093
|
+
function readPositiveMs(name, fallback) {
|
|
21094
|
+
const raw = (process.env[name] ?? "").trim();
|
|
21095
|
+
const parsed = Number.parseInt(raw, 10);
|
|
21096
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
21097
|
+
}
|
|
21098
|
+
function suspendTickMs() {
|
|
21099
|
+
return readPositiveMs("MRRLIN_DIRECTOR_BRIDGE_SUSPEND_TICK_MS", DEFAULT_SUSPEND_TICK_MS);
|
|
21100
|
+
}
|
|
21101
|
+
function suspendGapMs() {
|
|
21102
|
+
return readPositiveMs("MRRLIN_DIRECTOR_BRIDGE_SUSPEND_GAP_MS", DEFAULT_SUSPEND_GAP_MS);
|
|
21103
|
+
}
|
|
21104
|
+
function createSuspensionDetector(opts) {
|
|
21105
|
+
const tickMs = opts?.tickMs ?? suspendTickMs();
|
|
21106
|
+
const gapMs = opts?.gapMs ?? suspendGapMs();
|
|
21107
|
+
const now = opts?.now ?? Date.now;
|
|
21108
|
+
const setI = opts?.setIntervalFn ?? setInterval;
|
|
21109
|
+
const clearI = opts?.clearIntervalFn ?? clearInterval;
|
|
21110
|
+
let last = now();
|
|
21111
|
+
let suspended = 0;
|
|
21112
|
+
const timer = setI(() => {
|
|
21113
|
+
const t = now();
|
|
21114
|
+
const gap = t - last;
|
|
21115
|
+
last = t;
|
|
21116
|
+
if (gap >= gapMs) suspended += gap - tickMs;
|
|
21117
|
+
}, tickMs);
|
|
21118
|
+
timer.unref?.();
|
|
21119
|
+
return {
|
|
21120
|
+
suspendedMs: () => suspended,
|
|
21121
|
+
dispose: () => clearI(timer)
|
|
21122
|
+
};
|
|
21123
|
+
}
|
|
21124
|
+
|
|
21080
21125
|
// src/replay-state-machine.ts
|
|
21081
21126
|
function transition(state, event) {
|
|
21082
21127
|
if (event.kind === "resume" && !event.epochMatch) return { next: "invalid" };
|
|
@@ -21084,7 +21129,12 @@ function transition(state, event) {
|
|
|
21084
21129
|
case "live":
|
|
21085
21130
|
switch (event.kind) {
|
|
21086
21131
|
case "socket_close":
|
|
21087
|
-
return { next: "
|
|
21132
|
+
return { next: "detached", sideEffects: [] };
|
|
21133
|
+
case "resume":
|
|
21134
|
+
return {
|
|
21135
|
+
next: "live",
|
|
21136
|
+
sideEffects: ["rebind_sink_increment_epoch", "replay_drain"]
|
|
21137
|
+
};
|
|
21088
21138
|
case "codex_terminal":
|
|
21089
21139
|
return { next: "completed", sideEffects: ["mark_finished"] };
|
|
21090
21140
|
case "stop":
|
|
@@ -21095,35 +21145,22 @@ function transition(state, event) {
|
|
|
21095
21145
|
default:
|
|
21096
21146
|
return { next: "invalid" };
|
|
21097
21147
|
}
|
|
21098
|
-
case "
|
|
21148
|
+
case "detached":
|
|
21099
21149
|
switch (event.kind) {
|
|
21150
|
+
case "socket_close":
|
|
21151
|
+
return { next: "detached", sideEffects: [] };
|
|
21152
|
+
// idempotent
|
|
21100
21153
|
case "resume":
|
|
21101
21154
|
return {
|
|
21102
21155
|
next: "live",
|
|
21103
|
-
sideEffects: [
|
|
21104
|
-
"cancel_grace_timer",
|
|
21105
|
-
"rebind_sink_increment_epoch",
|
|
21106
|
-
"replay_drain"
|
|
21107
|
-
]
|
|
21108
|
-
};
|
|
21109
|
-
case "grace_expire":
|
|
21110
|
-
return {
|
|
21111
|
-
next: "interrupted",
|
|
21112
|
-
sideEffects: ["record_interrupted", "fire_turn_interrupt"]
|
|
21156
|
+
sideEffects: ["rebind_sink_increment_epoch", "replay_drain"]
|
|
21113
21157
|
};
|
|
21158
|
+
case "codex_terminal":
|
|
21159
|
+
return { next: "completed", sideEffects: ["mark_finished"] };
|
|
21114
21160
|
case "stop":
|
|
21115
21161
|
return {
|
|
21116
21162
|
next: "interrupted",
|
|
21117
|
-
sideEffects: [
|
|
21118
|
-
"cancel_grace_timer",
|
|
21119
|
-
"record_interrupted",
|
|
21120
|
-
"fire_turn_interrupt"
|
|
21121
|
-
]
|
|
21122
|
-
};
|
|
21123
|
-
case "codex_terminal":
|
|
21124
|
-
return {
|
|
21125
|
-
next: "completed",
|
|
21126
|
-
sideEffects: ["cancel_grace_timer", "mark_finished"]
|
|
21163
|
+
sideEffects: ["record_interrupted", "fire_turn_interrupt"]
|
|
21127
21164
|
};
|
|
21128
21165
|
default:
|
|
21129
21166
|
return { next: "invalid" };
|
|
@@ -36481,10 +36518,12 @@ var mrrlinTaskStatusSchema = external_exports.enum([
|
|
|
36481
36518
|
"closed_done",
|
|
36482
36519
|
"closed_no_result"
|
|
36483
36520
|
]);
|
|
36521
|
+
var mrrlinTerminalTaskStatuses = ["closed_done", "closed_no_result"];
|
|
36484
36522
|
var mrrlinTaskTypeSchema = external_exports.enum(["task", "bug"]);
|
|
36485
36523
|
var mrrlinTaskImpactSchema = external_exports.enum(["low", "medium", "high"]);
|
|
36486
36524
|
var mrrlinAutonomyLevelSchema = external_exports.enum(["auto", "minimal_human", "human_only"]);
|
|
36487
36525
|
var mrrlinAutoDeploySchema = external_exports.enum(["off", "dev", "prod"]);
|
|
36526
|
+
var mrrlinTaskDomainSchema = external_exports.enum(["code", "seo", "marketing", "content"]);
|
|
36488
36527
|
var mrrlinSpecApprovedSchema = external_exports.enum(["pending", "approved", "skipped", "skipped_legacy"]);
|
|
36489
36528
|
var mrrlinTaskSourceSchema = external_exports.enum(["fixture-import", "api"]);
|
|
36490
36529
|
var mrrlinPlanStatusSchema = external_exports.enum(["active", "paused", "done", "archived"]);
|
|
@@ -36516,6 +36555,27 @@ function normalizeTaskTags(tags) {
|
|
|
36516
36555
|
}
|
|
36517
36556
|
return [...normalized].sort();
|
|
36518
36557
|
}
|
|
36558
|
+
var terminalTaskStatusSet = new Set(mrrlinTerminalTaskStatuses);
|
|
36559
|
+
function isTaskTerminalStatus(status) {
|
|
36560
|
+
return terminalTaskStatusSet.has(status);
|
|
36561
|
+
}
|
|
36562
|
+
function getActiveBlockedByIds(blockedBy, blockers) {
|
|
36563
|
+
const blockersById = /* @__PURE__ */ new Map();
|
|
36564
|
+
for (const blocker of blockers) {
|
|
36565
|
+
if (!blocker)
|
|
36566
|
+
continue;
|
|
36567
|
+
blockersById.set(blocker.id, blocker.status);
|
|
36568
|
+
}
|
|
36569
|
+
const active = [];
|
|
36570
|
+
for (const blockerId of blockedBy ?? []) {
|
|
36571
|
+
const status = blockersById.get(blockerId);
|
|
36572
|
+
if (!status)
|
|
36573
|
+
continue;
|
|
36574
|
+
if (!isTaskTerminalStatus(status))
|
|
36575
|
+
active.push(blockerId);
|
|
36576
|
+
}
|
|
36577
|
+
return active;
|
|
36578
|
+
}
|
|
36519
36579
|
var mrrlinTaskTagSchema = external_exports.string().min(1).max(40).transform(normalizeTaskTagInput).pipe(external_exports.string().min(1).max(40));
|
|
36520
36580
|
var mrrlinTaskTagsSchema = external_exports.array(mrrlinTaskTagSchema).default([]).transform(normalizeTaskTags);
|
|
36521
36581
|
var mrrlinExecutionRunStatusSchema = external_exports.enum([
|
|
@@ -36569,6 +36629,10 @@ var mrrlinTaskAssigneeSchema = external_exports.union([external_exports.string()
|
|
|
36569
36629
|
var mrrlinTaskSchema = external_exports.object({
|
|
36570
36630
|
assignee: mrrlinTaskAssigneeSchema,
|
|
36571
36631
|
autoDeploy: mrrlinAutoDeploySchema,
|
|
36632
|
+
// ADR 0020. OUTPUT-optional (not defaulted-required) so hardcoded task fixtures/mocks across the
|
|
36633
|
+
// codebase need no churn; the DB column is NOT NULL default 'code', so real rows always carry it,
|
|
36634
|
+
// and resolveHeads() treats an absent domain as `code`. Persisted create/update use their own forms.
|
|
36635
|
+
domain: mrrlinTaskDomainSchema.optional(),
|
|
36572
36636
|
autonomyLevel: mrrlinAutonomyLevelSchema,
|
|
36573
36637
|
autonomyReason: external_exports.string().min(1).max(240),
|
|
36574
36638
|
blockedBy: external_exports.array(mrrlinTaskIdSchema),
|
|
@@ -36686,7 +36750,10 @@ var mrrlinProjectSummarySchema = external_exports.object({
|
|
|
36686
36750
|
emoji: mrrlinProjectEmojiSchema,
|
|
36687
36751
|
github: mrrlinGithubBindingSchema,
|
|
36688
36752
|
onboardingKickoffState: external_exports.enum(["pending", "sent"]).nullable().optional(),
|
|
36689
|
-
slug: mrrlinProjectSlugSchema
|
|
36753
|
+
slug: mrrlinProjectSlugSchema,
|
|
36754
|
+
// ADR 0020: default consensus domain for this project's plan/project-scoped specs. Output-optional
|
|
36755
|
+
// (DB column is NOT NULL default 'code') so existing project fixtures/upsert callers need no churn.
|
|
36756
|
+
defaultDomain: mrrlinTaskDomainSchema.optional()
|
|
36690
36757
|
});
|
|
36691
36758
|
var mrrlinProjectUpsertSchema = mrrlinProjectSummarySchema.omit({
|
|
36692
36759
|
onboardingKickoffState: true,
|
|
@@ -36735,6 +36802,9 @@ var mrrlinProjectSnapshotSchema = external_exports.object({
|
|
|
36735
36802
|
});
|
|
36736
36803
|
var mrrlinTaskCreateSchema = external_exports.object({
|
|
36737
36804
|
autoDeploy: mrrlinAutoDeploySchema.default("off"),
|
|
36805
|
+
// Input-optional so existing createTask call sites need no churn; createTask fills 'code' for the
|
|
36806
|
+
// INSERT (the pg column is NOT NULL, so a concrete value is required, not undefined→NULL).
|
|
36807
|
+
domain: mrrlinTaskDomainSchema.optional(),
|
|
36738
36808
|
autonomyLevel: mrrlinAutonomyLevelSchema.default("minimal_human"),
|
|
36739
36809
|
autonomyReason: external_exports.string().min(1).max(240).default("Needs human review before automation."),
|
|
36740
36810
|
category: taxonomyCategorySchema,
|
|
@@ -36758,6 +36828,7 @@ var mrrlinTaskUpdateSchema = external_exports.object({
|
|
|
36758
36828
|
actor: external_exports.string().min(1).optional(),
|
|
36759
36829
|
assignee: mrrlinTaskAssigneeSchema.optional(),
|
|
36760
36830
|
autoDeploy: mrrlinAutoDeploySchema.optional(),
|
|
36831
|
+
domain: mrrlinTaskDomainSchema.optional(),
|
|
36761
36832
|
autonomyLevel: mrrlinAutonomyLevelSchema.optional(),
|
|
36762
36833
|
autonomyReason: external_exports.string().min(1).max(240).optional(),
|
|
36763
36834
|
category: taxonomyCategorySchema.optional(),
|
|
@@ -37391,6 +37462,43 @@ var mrrlinInboxItemActionPayloadSchema = external_exports.object({
|
|
|
37391
37462
|
// (worktree + push/PR path). Scheduler proposals set it; legacy payloads omit it.
|
|
37392
37463
|
codeExecution: external_exports.boolean().optional()
|
|
37393
37464
|
});
|
|
37465
|
+
var mrrlinInboxQuestionOptionSchema = external_exports.object({
|
|
37466
|
+
answer: external_exports.string().trim().min(1),
|
|
37467
|
+
description: external_exports.string().trim().min(1),
|
|
37468
|
+
label: external_exports.string().trim().min(1),
|
|
37469
|
+
recommended: external_exports.boolean().optional()
|
|
37470
|
+
});
|
|
37471
|
+
var mrrlinInboxQuestionOptionsSchema = external_exports.array(mrrlinInboxQuestionOptionSchema).min(1).max(3).superRefine((options, context) => {
|
|
37472
|
+
const labels = /* @__PURE__ */ new Set();
|
|
37473
|
+
const answers = /* @__PURE__ */ new Set();
|
|
37474
|
+
let recommendedCount = 0;
|
|
37475
|
+
for (const [index, option] of options.entries()) {
|
|
37476
|
+
if (labels.has(option.label)) {
|
|
37477
|
+
context.addIssue({
|
|
37478
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37479
|
+
message: "questionOptions labels must be unique after trimming",
|
|
37480
|
+
path: [index, "label"]
|
|
37481
|
+
});
|
|
37482
|
+
}
|
|
37483
|
+
labels.add(option.label);
|
|
37484
|
+
if (answers.has(option.answer)) {
|
|
37485
|
+
context.addIssue({
|
|
37486
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37487
|
+
message: "questionOptions answers must be unique after trimming",
|
|
37488
|
+
path: [index, "answer"]
|
|
37489
|
+
});
|
|
37490
|
+
}
|
|
37491
|
+
answers.add(option.answer);
|
|
37492
|
+
if (option.recommended === true)
|
|
37493
|
+
recommendedCount += 1;
|
|
37494
|
+
}
|
|
37495
|
+
if (recommendedCount > 1) {
|
|
37496
|
+
context.addIssue({
|
|
37497
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37498
|
+
message: "questionOptions may mark at most one option as recommended"
|
|
37499
|
+
});
|
|
37500
|
+
}
|
|
37501
|
+
});
|
|
37394
37502
|
var mrrlinInboxItemSchema = external_exports.object({
|
|
37395
37503
|
actionPayload: mrrlinInboxItemActionPayloadSchema.nullable(),
|
|
37396
37504
|
answer: external_exports.string().nullable(),
|
|
@@ -37406,9 +37514,18 @@ var mrrlinInboxItemSchema = external_exports.object({
|
|
|
37406
37514
|
origin: external_exports.string().nullable().optional(),
|
|
37407
37515
|
planId: mrrlinPlanIdSchema.nullable().default(null),
|
|
37408
37516
|
projectSlug: mrrlinProjectSlugSchema,
|
|
37517
|
+
questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().default(null),
|
|
37409
37518
|
readAt: external_exports.string().datetime().nullable(),
|
|
37410
37519
|
status: mrrlinInboxItemStatusSchema,
|
|
37411
37520
|
taskId: mrrlinTaskIdSchema.nullable()
|
|
37521
|
+
}).superRefine((value, context) => {
|
|
37522
|
+
if (value.kind !== "question" && value.questionOptions !== null) {
|
|
37523
|
+
context.addIssue({
|
|
37524
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37525
|
+
message: "questionOptions are allowed only on question inbox items",
|
|
37526
|
+
path: ["questionOptions"]
|
|
37527
|
+
});
|
|
37528
|
+
}
|
|
37412
37529
|
});
|
|
37413
37530
|
var mrrlinInboxItemCreateSchema = external_exports.object({
|
|
37414
37531
|
actionPayload: mrrlinInboxItemActionPayloadSchema.nullable().optional(),
|
|
@@ -37418,7 +37535,16 @@ var mrrlinInboxItemCreateSchema = external_exports.object({
|
|
|
37418
37535
|
kind: mrrlinInboxItemKindSchema,
|
|
37419
37536
|
origin: external_exports.string().min(1).optional(),
|
|
37420
37537
|
planId: mrrlinPlanIdSchema.nullable().optional(),
|
|
37538
|
+
questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().optional(),
|
|
37421
37539
|
taskId: mrrlinTaskIdSchema.nullable().optional()
|
|
37540
|
+
}).superRefine((value, context) => {
|
|
37541
|
+
if (value.kind !== "question" && value.questionOptions != null) {
|
|
37542
|
+
context.addIssue({
|
|
37543
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37544
|
+
message: "questionOptions are allowed only on question inbox items",
|
|
37545
|
+
path: ["questionOptions"]
|
|
37546
|
+
});
|
|
37547
|
+
}
|
|
37422
37548
|
});
|
|
37423
37549
|
var mrrlinInboxItemDecideSchema = external_exports.object({
|
|
37424
37550
|
answer: external_exports.string().min(1).optional(),
|
|
@@ -38608,7 +38734,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
|
|
|
38608
38734
|
}
|
|
38609
38735
|
|
|
38610
38736
|
// src/_generated/version.ts
|
|
38611
|
-
var PKG_VERSION = "0.3.
|
|
38737
|
+
var PKG_VERSION = "0.3.20";
|
|
38612
38738
|
|
|
38613
38739
|
// src/api-base-url.ts
|
|
38614
38740
|
var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
|
|
@@ -39370,6 +39496,31 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39370
39496
|
return legacy != null ? Math.min(legacy, remaining) : remaining;
|
|
39371
39497
|
};
|
|
39372
39498
|
const task = await deps.client.getTask(projectSlug, claimed.taskId);
|
|
39499
|
+
const blockerTasks = task.blockedBy.length ? await Promise.all(task.blockedBy.map((blockerTaskId) => deps.client.getTask(projectSlug, blockerTaskId))) : [];
|
|
39500
|
+
const missingBlockers = task.blockedBy.filter((_, index) => !blockerTasks[index]);
|
|
39501
|
+
if (missingBlockers.length > 0) {
|
|
39502
|
+
await failRun(
|
|
39503
|
+
deps,
|
|
39504
|
+
projectSlug,
|
|
39505
|
+
task,
|
|
39506
|
+
runId,
|
|
39507
|
+
leaseId,
|
|
39508
|
+
`task_blocked: run ${runId} targets ${task.id} but blocker tasks could not be loaded: ${missingBlockers.join(", ")}`
|
|
39509
|
+
);
|
|
39510
|
+
return;
|
|
39511
|
+
}
|
|
39512
|
+
const activeBlockers = getActiveBlockedByIds(task.blockedBy, blockerTasks);
|
|
39513
|
+
if (activeBlockers.length > 0) {
|
|
39514
|
+
await failRun(
|
|
39515
|
+
deps,
|
|
39516
|
+
projectSlug,
|
|
39517
|
+
task,
|
|
39518
|
+
runId,
|
|
39519
|
+
leaseId,
|
|
39520
|
+
`task_blocked: run ${runId} targets ${task.id} but active blockers remain open: ${activeBlockers.join(", ")}`
|
|
39521
|
+
);
|
|
39522
|
+
return;
|
|
39523
|
+
}
|
|
39373
39524
|
const compactEnabled = (process.env.MRRLIN_MCP_COMPACT_DISABLE ?? "").trim() !== "1";
|
|
39374
39525
|
const context = await gatherContext(deps.client, projectSlug, task, runId);
|
|
39375
39526
|
const bundle = renderContextBundle(
|
|
@@ -39391,7 +39542,11 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39391
39542
|
};
|
|
39392
39543
|
if (claimed.codeExecution) {
|
|
39393
39544
|
if (!checkout) {
|
|
39394
|
-
const reason = `local_checkout_missing: run ${runId}
|
|
39545
|
+
const reason = `local_checkout_missing: run ${runId} needs code execution, but project ${projectSlug} has no local git checkout registered on this machine, so there is nowhere to make the changes. Explain this to the operator in plain, non-technical language, then proactively help register one:
|
|
39546
|
+
1. Search the usual locations (~/dev, ~/code, ~/work, ~/projects) for a clone of this project's GitHub repo, matching by its \`git remote origin\` URL.
|
|
39547
|
+
2. If you find a likely match, tell the operator where you think it is and ask them to confirm \u2014 e.g. "For code changes I need this project's repo checked out here. I think it's at <path> \u2014 should I register that one, or do you want to point me at a different path?"
|
|
39548
|
+
3. When the operator confirms a path, call register_local_checkout({ projectSlug: "${projectSlug}", path: "<absolute path>", repo: "<this project's GitHub repo>" }).
|
|
39549
|
+
4. If no candidate is found, ask the operator for the absolute path to the checkout (or to clone the repo first), then register it. Once a checkout is registered, the run can be re-queued.`;
|
|
39395
39550
|
await failRun(deps, projectSlug, task, runId, leaseId, reason);
|
|
39396
39551
|
return;
|
|
39397
39552
|
}
|
|
@@ -40037,9 +40192,15 @@ function saveThreadIndex(stateDir, entries) {
|
|
|
40037
40192
|
function createInactivityWatchdog(opts) {
|
|
40038
40193
|
const setT = opts.setTimeoutFn ?? setTimeout;
|
|
40039
40194
|
const clearT = opts.clearTimeoutFn ?? clearTimeout;
|
|
40195
|
+
const now = opts.now ?? Date.now;
|
|
40196
|
+
const suspended = opts.suspendedMs ?? (() => 0);
|
|
40040
40197
|
let idleTimer = null;
|
|
40041
40198
|
let maxTimer = null;
|
|
40042
40199
|
let done = false;
|
|
40200
|
+
const startAt = now();
|
|
40201
|
+
const suspendedAtStart = suspended();
|
|
40202
|
+
let lastPokeAt = startAt;
|
|
40203
|
+
let suspendedAtLastPoke = suspendedAtStart;
|
|
40043
40204
|
const expire = (reason) => {
|
|
40044
40205
|
if (done) return;
|
|
40045
40206
|
done = true;
|
|
@@ -40047,16 +40208,41 @@ function createInactivityWatchdog(opts) {
|
|
|
40047
40208
|
if (maxTimer) clearT(maxTimer);
|
|
40048
40209
|
opts.onExpire(reason);
|
|
40049
40210
|
};
|
|
40050
|
-
|
|
40051
|
-
|
|
40052
|
-
|
|
40053
|
-
|
|
40211
|
+
const armIdle = (ms) => {
|
|
40212
|
+
idleTimer = setT(onIdleDeadline, Math.max(1, ms));
|
|
40213
|
+
idleTimer.unref?.();
|
|
40214
|
+
};
|
|
40215
|
+
const armMax = (ms) => {
|
|
40216
|
+
maxTimer = setT(onMaxDeadline, Math.max(1, ms));
|
|
40217
|
+
maxTimer.unref?.();
|
|
40218
|
+
};
|
|
40219
|
+
const onIdleDeadline = () => {
|
|
40220
|
+
if (done) return;
|
|
40221
|
+
const awakeSilence = now() - lastPokeAt - (suspended() - suspendedAtLastPoke);
|
|
40222
|
+
if (awakeSilence < opts.idleMs) {
|
|
40223
|
+
armIdle(opts.idleMs - awakeSilence);
|
|
40224
|
+
return;
|
|
40225
|
+
}
|
|
40226
|
+
expire("idle");
|
|
40227
|
+
};
|
|
40228
|
+
const onMaxDeadline = () => {
|
|
40229
|
+
if (done) return;
|
|
40230
|
+
const awakeElapsed = now() - startAt - (suspended() - suspendedAtStart);
|
|
40231
|
+
if (awakeElapsed < opts.maxMs) {
|
|
40232
|
+
armMax(opts.maxMs - awakeElapsed);
|
|
40233
|
+
return;
|
|
40234
|
+
}
|
|
40235
|
+
expire("max");
|
|
40236
|
+
};
|
|
40237
|
+
armIdle(opts.idleMs);
|
|
40238
|
+
armMax(opts.maxMs);
|
|
40054
40239
|
return {
|
|
40055
40240
|
poke: () => {
|
|
40056
40241
|
if (done) return;
|
|
40057
40242
|
if (idleTimer) clearT(idleTimer);
|
|
40058
|
-
|
|
40059
|
-
|
|
40243
|
+
lastPokeAt = now();
|
|
40244
|
+
suspendedAtLastPoke = suspended();
|
|
40245
|
+
armIdle(opts.idleMs);
|
|
40060
40246
|
},
|
|
40061
40247
|
cancel: () => {
|
|
40062
40248
|
done = true;
|
|
@@ -44232,6 +44418,7 @@ function createBridgeLogger(stateDir, env = process.env) {
|
|
|
44232
44418
|
}
|
|
44233
44419
|
|
|
44234
44420
|
// src/director-bridge.ts
|
|
44421
|
+
var JSON_GPT_MODEL_REF_RE = /"(?:slug|model)"\s*:\s*"(gpt-[a-z0-9.-]+)"/gi;
|
|
44235
44422
|
var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
|
|
44236
44423
|
"http://localhost:*",
|
|
44237
44424
|
"http://127.0.0.1:*",
|
|
@@ -44252,6 +44439,139 @@ function normalizeModel(value) {
|
|
|
44252
44439
|
const trimmed = value.trim();
|
|
44253
44440
|
return trimmed ? trimmed : null;
|
|
44254
44441
|
}
|
|
44442
|
+
function isSelectableModelSlug(value) {
|
|
44443
|
+
return typeof value === "string" && value.startsWith("gpt-");
|
|
44444
|
+
}
|
|
44445
|
+
function humanizeModelSlug(slug) {
|
|
44446
|
+
if (!slug.startsWith("gpt-")) return slug;
|
|
44447
|
+
const rest = slug.slice(4);
|
|
44448
|
+
if (!rest) return "GPT";
|
|
44449
|
+
const [version2, ...variants] = rest.split("-").filter(Boolean);
|
|
44450
|
+
if (variants.length === 0) return `GPT-${version2}`;
|
|
44451
|
+
return `GPT-${version2} ${variants.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ")}`;
|
|
44452
|
+
}
|
|
44453
|
+
function mapBridgeModel(raw) {
|
|
44454
|
+
const model = (raw.model ?? raw.id ?? "").trim();
|
|
44455
|
+
if (!model) return null;
|
|
44456
|
+
return {
|
|
44457
|
+
id: (raw.id ?? model).trim() || model,
|
|
44458
|
+
model,
|
|
44459
|
+
displayName: (raw.displayName ?? model).trim() || model,
|
|
44460
|
+
description: raw.description ?? "",
|
|
44461
|
+
supportedReasoningEfforts: (raw.supportedReasoningEfforts ?? []).map((entry) => ({
|
|
44462
|
+
reasoningEffort: entry.reasoningEffort,
|
|
44463
|
+
description: entry.description ?? ""
|
|
44464
|
+
})),
|
|
44465
|
+
defaultReasoningEffort: raw.defaultReasoningEffort ?? null,
|
|
44466
|
+
isDefault: raw.isDefault === true
|
|
44467
|
+
};
|
|
44468
|
+
}
|
|
44469
|
+
function addBridgeModel(target, model) {
|
|
44470
|
+
if (!model) return;
|
|
44471
|
+
if (!target.has(model.model)) target.set(model.model, model);
|
|
44472
|
+
}
|
|
44473
|
+
function readTextIfExists(filePath) {
|
|
44474
|
+
try {
|
|
44475
|
+
return import_node_fs10.default.readFileSync(filePath, "utf8");
|
|
44476
|
+
} catch {
|
|
44477
|
+
return null;
|
|
44478
|
+
}
|
|
44479
|
+
}
|
|
44480
|
+
function readKnownGptSlugsFromTomlFile(filePath) {
|
|
44481
|
+
const raw = readTextIfExists(filePath);
|
|
44482
|
+
if (!raw) return [];
|
|
44483
|
+
return Array.from(raw.matchAll(/^\s*model\s*=\s*"([^"]+)"/gm)).map((match) => match[1]?.trim() ?? "").filter(isSelectableModelSlug);
|
|
44484
|
+
}
|
|
44485
|
+
function readCodexFallbackModels() {
|
|
44486
|
+
const codexHome = process.env.CODEX_HOME?.trim() || import_node_path11.default.join(import_node_os7.default.homedir(), ".codex");
|
|
44487
|
+
const models = /* @__PURE__ */ new Map();
|
|
44488
|
+
const cachePath = import_node_path11.default.join(codexHome, "models_cache.json");
|
|
44489
|
+
const cacheRaw = readTextIfExists(cachePath);
|
|
44490
|
+
if (cacheRaw) {
|
|
44491
|
+
try {
|
|
44492
|
+
const parsed = JSON.parse(cacheRaw);
|
|
44493
|
+
if (Array.isArray(parsed.models)) {
|
|
44494
|
+
for (const entry of parsed.models) {
|
|
44495
|
+
if (!entry || typeof entry !== "object") continue;
|
|
44496
|
+
const cached2 = entry;
|
|
44497
|
+
const slug = typeof cached2.slug === "string" ? cached2.slug.trim() : "";
|
|
44498
|
+
if (!isSelectableModelSlug(slug)) continue;
|
|
44499
|
+
const supportedReasoningEfforts = Array.isArray(cached2.supported_reasoning_levels) ? cached2.supported_reasoning_levels.flatMap((level) => {
|
|
44500
|
+
if (!level || typeof level !== "object") return [];
|
|
44501
|
+
const option = level;
|
|
44502
|
+
if (typeof option.effort !== "string" || !option.effort.trim()) return [];
|
|
44503
|
+
return [{ reasoningEffort: option.effort, description: typeof option.description === "string" ? option.description : "" }];
|
|
44504
|
+
}) : [];
|
|
44505
|
+
addBridgeModel(
|
|
44506
|
+
models,
|
|
44507
|
+
mapBridgeModel({
|
|
44508
|
+
id: slug,
|
|
44509
|
+
model: slug,
|
|
44510
|
+
displayName: typeof cached2.display_name === "string" ? cached2.display_name : humanizeModelSlug(slug),
|
|
44511
|
+
description: typeof cached2.description === "string" ? cached2.description : "",
|
|
44512
|
+
supportedReasoningEfforts,
|
|
44513
|
+
defaultReasoningEffort: typeof cached2.default_reasoning_level === "string" ? cached2.default_reasoning_level : null,
|
|
44514
|
+
isDefault: false
|
|
44515
|
+
})
|
|
44516
|
+
);
|
|
44517
|
+
}
|
|
44518
|
+
}
|
|
44519
|
+
} catch {
|
|
44520
|
+
}
|
|
44521
|
+
for (const match of cacheRaw.matchAll(JSON_GPT_MODEL_REF_RE)) {
|
|
44522
|
+
const slug = match[1]?.trim().toLowerCase() ?? "";
|
|
44523
|
+
if (!isSelectableModelSlug(slug)) continue;
|
|
44524
|
+
addBridgeModel(
|
|
44525
|
+
models,
|
|
44526
|
+
mapBridgeModel({
|
|
44527
|
+
id: slug,
|
|
44528
|
+
model: slug,
|
|
44529
|
+
displayName: humanizeModelSlug(slug),
|
|
44530
|
+
description: "",
|
|
44531
|
+
supportedReasoningEfforts: [],
|
|
44532
|
+
defaultReasoningEffort: null,
|
|
44533
|
+
isDefault: false
|
|
44534
|
+
})
|
|
44535
|
+
);
|
|
44536
|
+
}
|
|
44537
|
+
}
|
|
44538
|
+
for (const slug of readKnownGptSlugsFromTomlFile(import_node_path11.default.join(codexHome, "config.toml"))) {
|
|
44539
|
+
addBridgeModel(
|
|
44540
|
+
models,
|
|
44541
|
+
mapBridgeModel({
|
|
44542
|
+
id: slug,
|
|
44543
|
+
model: slug,
|
|
44544
|
+
displayName: humanizeModelSlug(slug),
|
|
44545
|
+
description: "",
|
|
44546
|
+
supportedReasoningEfforts: [],
|
|
44547
|
+
defaultReasoningEffort: null,
|
|
44548
|
+
isDefault: false
|
|
44549
|
+
})
|
|
44550
|
+
);
|
|
44551
|
+
}
|
|
44552
|
+
const automationsDir = import_node_path11.default.join(codexHome, "automations");
|
|
44553
|
+
try {
|
|
44554
|
+
for (const entry of import_node_fs10.default.readdirSync(automationsDir, { withFileTypes: true })) {
|
|
44555
|
+
if (!entry.isDirectory()) continue;
|
|
44556
|
+
for (const slug of readKnownGptSlugsFromTomlFile(import_node_path11.default.join(automationsDir, entry.name, "automation.toml"))) {
|
|
44557
|
+
addBridgeModel(
|
|
44558
|
+
models,
|
|
44559
|
+
mapBridgeModel({
|
|
44560
|
+
id: slug,
|
|
44561
|
+
model: slug,
|
|
44562
|
+
displayName: humanizeModelSlug(slug),
|
|
44563
|
+
description: "",
|
|
44564
|
+
supportedReasoningEfforts: [],
|
|
44565
|
+
defaultReasoningEffort: null,
|
|
44566
|
+
isDefault: false
|
|
44567
|
+
})
|
|
44568
|
+
);
|
|
44569
|
+
}
|
|
44570
|
+
}
|
|
44571
|
+
} catch {
|
|
44572
|
+
}
|
|
44573
|
+
return [...models.values()];
|
|
44574
|
+
}
|
|
44255
44575
|
function neutralCheckoutCwd(stateDir) {
|
|
44256
44576
|
const dir = import_node_path11.default.join(stateDir, "no-checkout");
|
|
44257
44577
|
try {
|
|
@@ -44288,7 +44608,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
|
|
|
44288
44608
|
" bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
|
|
44289
44609
|
"- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
|
|
44290
44610
|
"- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
|
|
44291
|
-
"- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a consensus `info` item says CONVERGED, proceed to `create_execution_run`. When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more.",
|
|
44611
|
+
"- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a consensus `info` item says CONVERGED, proceed to `create_execution_run`. When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more. For a NON-code task (domain seo/marketing/content, ADR 0020) the spec is review-only \u2014 do NOT call `create_execution_run` (non-code execution is not yet supported and fail-closes); tell the operator the spec was reviewed.",
|
|
44292
44612
|
'- When a task needs a browser (login flows, UI verification/clicks, scraping), pass `prerequisites:[{mcp:"mrrlin-browser",reason:"..."}]` to `create_execution_run`. If that run later fails into a handoff, the operator gets a one-click "Launch browser & resume" that brings up the dedicated browser as the turn starts.',
|
|
44293
44613
|
"- When you actually drive a browser (calling `browser_navigate`, `browser_click`, `browser_snapshot`, `browser_type`, etc.), invoke those tools from the **`mrrlin-browser`** MCP server ONLY. Do NOT use tools from any other browser MCP (`playwright`, `chrome-devtools`, `puppeteer`, etc.) even if they are also connected on this operator's box \u2014 those use a shared/default Playwright profile that is fragile across runs. Only `mrrlin-browser` uses this project's dedicated Chromium + persistent operator profile (`~/.mrrlin/browser/profile`). If your host exposes multiple browser MCPs with overlapping tool names, prefer the `mrrlin-browser` server unconditionally.",
|
|
44294
44614
|
"",
|
|
@@ -44357,8 +44677,12 @@ function buildThreadStartParams(msg, config2, desiredModel) {
|
|
|
44357
44677
|
" assumption on the spec page.",
|
|
44358
44678
|
"- For any such task-affecting question, ALSO bank it durably before your final reply:",
|
|
44359
44679
|
' `create_inbox_item` with `kind="question"`, `taskId` set, `directorSessionId` from the',
|
|
44360
|
-
" Context JSON,
|
|
44361
|
-
"
|
|
44680
|
+
" Context JSON, `body` = the exact question, and `questionOptions` = 2-3 concrete answer",
|
|
44681
|
+
" options. Each option MUST include a short `label`, a concise `description`, and a",
|
|
44682
|
+
" self-contained durable `answer`; you MAY mark at most one option as `recommended` when",
|
|
44683
|
+
" you explicitly prefer a path. The operator always has a custom-answer path in the UI, so",
|
|
44684
|
+
" your options should guide, not trap. (The server dedups identical open questions; do not",
|
|
44685
|
+
" supply a dedup key.)",
|
|
44362
44686
|
"- When the operator answers in chat, write the answer into the Specs/{taskId}",
|
|
44363
44687
|
' Clarifications section, then `decide_inbox_item` the question with `verdict="acknowledged"`',
|
|
44364
44688
|
" and `answer` = the resolved answer.",
|
|
@@ -44689,6 +45013,24 @@ function attachBridgeWss(server, opts) {
|
|
|
44689
45013
|
return;
|
|
44690
45014
|
}
|
|
44691
45015
|
const span = { id: "conn", at: 0 };
|
|
45016
|
+
let isAlive = true;
|
|
45017
|
+
socket.on("pong", () => {
|
|
45018
|
+
isAlive = true;
|
|
45019
|
+
});
|
|
45020
|
+
const pingTimer = setInterval(() => {
|
|
45021
|
+
if (socket.readyState !== socket.OPEN) return;
|
|
45022
|
+
if (!isAlive) {
|
|
45023
|
+
socket.terminate();
|
|
45024
|
+
return;
|
|
45025
|
+
}
|
|
45026
|
+
isAlive = false;
|
|
45027
|
+
try {
|
|
45028
|
+
socket.ping();
|
|
45029
|
+
} catch {
|
|
45030
|
+
}
|
|
45031
|
+
}, wsPingMs());
|
|
45032
|
+
pingTimer.unref?.();
|
|
45033
|
+
socket.on("close", () => clearInterval(pingTimer));
|
|
44692
45034
|
const send = (msg) => {
|
|
44693
45035
|
if (socket.readyState !== socket.OPEN) return;
|
|
44694
45036
|
logger?.logOut({
|
|
@@ -44732,6 +45074,20 @@ function attachBridgeWss(server, opts) {
|
|
|
44732
45074
|
}
|
|
44733
45075
|
var TurnInterruptedError = class extends Error {
|
|
44734
45076
|
};
|
|
45077
|
+
var DEFAULT_WS_PING_SECONDS = 30;
|
|
45078
|
+
function wsPingMs() {
|
|
45079
|
+
const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_WS_PING_SECONDS ?? "").trim();
|
|
45080
|
+
const parsed = Number.parseInt(raw, 10);
|
|
45081
|
+
return (Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_WS_PING_SECONDS) * 1e3;
|
|
45082
|
+
}
|
|
45083
|
+
async function waitFor(pred, timeoutMs) {
|
|
45084
|
+
const deadline = Date.now() + timeoutMs;
|
|
45085
|
+
while (Date.now() < deadline) {
|
|
45086
|
+
if (pred()) return true;
|
|
45087
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
45088
|
+
}
|
|
45089
|
+
return pred();
|
|
45090
|
+
}
|
|
44735
45091
|
function attachSeqMetadata(msg, seq, turnId) {
|
|
44736
45092
|
if (msg.type === "event" || msg.type === "rateLimits" || msg.type === "error") {
|
|
44737
45093
|
return { ...msg, seq, turnId };
|
|
@@ -44790,11 +45146,11 @@ function createBridgeMessageHandler(deps) {
|
|
|
44790
45146
|
logResumeRejected(req.directorSessionId, "superseded");
|
|
44791
45147
|
return;
|
|
44792
45148
|
}
|
|
44793
|
-
|
|
45149
|
+
let tail2 = eventsAfter(buf, req.lastSeq);
|
|
45150
|
+
let truncated = false;
|
|
44794
45151
|
if (tail2 === null) {
|
|
44795
|
-
|
|
44796
|
-
|
|
44797
|
-
return;
|
|
45152
|
+
truncated = true;
|
|
45153
|
+
tail2 = [...buf.events];
|
|
44798
45154
|
}
|
|
44799
45155
|
const result = transition(buf.state, { kind: "resume", epochMatch: true });
|
|
44800
45156
|
if (result.next === "invalid") {
|
|
@@ -44806,7 +45162,14 @@ function createBridgeMessageHandler(deps) {
|
|
|
44806
45162
|
buf.replaying = true;
|
|
44807
45163
|
deps.bindSessionSink?.(req.directorSessionId, send);
|
|
44808
45164
|
boundSessions.add(req.directorSessionId);
|
|
44809
|
-
if (buf.
|
|
45165
|
+
if (buf.triggerDetach) {
|
|
45166
|
+
const detach = buf.triggerDetach;
|
|
45167
|
+
const epochAtBind = buf.bindEpoch;
|
|
45168
|
+
socket.once("close", () => {
|
|
45169
|
+
if (buf.bindEpoch !== epochAtBind) return;
|
|
45170
|
+
detach();
|
|
45171
|
+
});
|
|
45172
|
+
}
|
|
44810
45173
|
const willStreamLive = result.next === "live";
|
|
44811
45174
|
sendMsg({
|
|
44812
45175
|
type: "resume",
|
|
@@ -44819,6 +45182,14 @@ function createBridgeMessageHandler(deps) {
|
|
|
44819
45182
|
eventCount: tail2.length,
|
|
44820
45183
|
willStreamLive
|
|
44821
45184
|
});
|
|
45185
|
+
if (truncated) {
|
|
45186
|
+
sendMsg({
|
|
45187
|
+
type: "replay-truncated",
|
|
45188
|
+
directorSessionId: req.directorSessionId,
|
|
45189
|
+
turnId: buf.turnId,
|
|
45190
|
+
droppedBelowSeq: tail2[0]?.seq ?? buf.lastSeq
|
|
45191
|
+
});
|
|
45192
|
+
}
|
|
44822
45193
|
for (const { msg: bufferedMsg } of tail2) sendMsg(bufferedMsg);
|
|
44823
45194
|
for (const m of buf.outboundQueue) sendMsg(m);
|
|
44824
45195
|
buf.outboundQueue.length = 0;
|
|
@@ -44897,26 +45268,36 @@ function createBridgeMessageHandler(deps) {
|
|
|
44897
45268
|
let models = null;
|
|
44898
45269
|
let rateLimits = null;
|
|
44899
45270
|
let bootstrapError;
|
|
45271
|
+
const fallbackModels = readCodexFallbackModels();
|
|
44900
45272
|
try {
|
|
44901
45273
|
const codexClient2 = await deps.acquireCodex();
|
|
44902
45274
|
try {
|
|
44903
45275
|
try {
|
|
44904
|
-
const res = await codexClient2.model.list({});
|
|
45276
|
+
const res = await codexClient2.model.list({ includeHidden: true });
|
|
44905
45277
|
const raw = res.data ?? res.models ?? [];
|
|
44906
|
-
|
|
44907
|
-
|
|
44908
|
-
|
|
44909
|
-
|
|
44910
|
-
|
|
44911
|
-
|
|
44912
|
-
|
|
44913
|
-
|
|
44914
|
-
|
|
44915
|
-
|
|
44916
|
-
|
|
44917
|
-
|
|
45278
|
+
const mergedModels = /* @__PURE__ */ new Map();
|
|
45279
|
+
for (const m of raw) {
|
|
45280
|
+
const modelSlug = (m.model ?? m.id ?? "").trim();
|
|
45281
|
+
if (!modelSlug) continue;
|
|
45282
|
+
if (m.hidden === true && (modelSlug.startsWith("codex-") || !modelSlug.startsWith("gpt-"))) continue;
|
|
45283
|
+
addBridgeModel(
|
|
45284
|
+
mergedModels,
|
|
45285
|
+
mapBridgeModel({
|
|
45286
|
+
id: m.id,
|
|
45287
|
+
model: m.model ?? m.id,
|
|
45288
|
+
displayName: m.displayName ?? m.model ?? m.id,
|
|
45289
|
+
description: m.description ?? "",
|
|
45290
|
+
supportedReasoningEfforts: m.supportedReasoningEfforts ?? [],
|
|
45291
|
+
defaultReasoningEffort: m.defaultReasoningEffort ?? null,
|
|
45292
|
+
isDefault: m.isDefault === true
|
|
45293
|
+
})
|
|
45294
|
+
);
|
|
45295
|
+
}
|
|
45296
|
+
for (const fallbackModel of fallbackModels) addBridgeModel(mergedModels, fallbackModel);
|
|
45297
|
+
models = [...mergedModels.values()];
|
|
44918
45298
|
} catch (error51) {
|
|
44919
45299
|
bootstrapError = error51 instanceof Error ? error51.message : String(error51);
|
|
45300
|
+
models = fallbackModels.length > 0 ? fallbackModels : null;
|
|
44920
45301
|
}
|
|
44921
45302
|
try {
|
|
44922
45303
|
const res = await codexClient2.account.readRateLimits();
|
|
@@ -44929,6 +45310,7 @@ function createBridgeMessageHandler(deps) {
|
|
|
44929
45310
|
}
|
|
44930
45311
|
} catch (error51) {
|
|
44931
45312
|
bootstrapError = error51 instanceof Error ? error51.message : String(error51);
|
|
45313
|
+
models = fallbackModels.length > 0 ? fallbackModels : null;
|
|
44932
45314
|
}
|
|
44933
45315
|
sendForSpan({ type: "bootstrap", models, rateLimits, ...deps.bridgeInstanceId ? { bridgeInstanceId: deps.bridgeInstanceId } : {}, ...bootstrapError ? { error: bootstrapError } : {} });
|
|
44934
45316
|
return;
|
|
@@ -44937,6 +45319,20 @@ function createBridgeMessageHandler(deps) {
|
|
|
44937
45319
|
handleResumeRequest(msg, sendForSpan);
|
|
44938
45320
|
return;
|
|
44939
45321
|
}
|
|
45322
|
+
if (msg.type === "ack") {
|
|
45323
|
+
const ack = msg;
|
|
45324
|
+
const sessionId = typeof ack.directorSessionId === "string" ? ack.directorSessionId.trim() : "";
|
|
45325
|
+
const ackThread = sessionId ? deps.threads.get(sessionId) : void 0;
|
|
45326
|
+
if (ackThread?.replay && typeof ack.turnId === "string" && typeof ack.lastSeq === "number" && recordAck(ackThread.replay, ack.turnId, ack.lastSeq, Date.now())) {
|
|
45327
|
+
logCtx?.logger.logOut({
|
|
45328
|
+
spanId,
|
|
45329
|
+
sessionId,
|
|
45330
|
+
type: "<result-acked>",
|
|
45331
|
+
payload: { sessionId, turnId: ack.turnId, lastSeq: ack.lastSeq }
|
|
45332
|
+
});
|
|
45333
|
+
}
|
|
45334
|
+
return;
|
|
45335
|
+
}
|
|
44940
45336
|
if (msg.type === "interrupt") {
|
|
44941
45337
|
const directorSessionId2 = (msg.directorSessionId ?? "").trim();
|
|
44942
45338
|
const existing = directorSessionId2 ? deps.threads.get(directorSessionId2) : void 0;
|
|
@@ -44945,12 +45341,11 @@ function createBridgeMessageHandler(deps) {
|
|
|
44945
45341
|
return;
|
|
44946
45342
|
}
|
|
44947
45343
|
const interruptBuf = existing.replay;
|
|
44948
|
-
if (interruptBuf && interruptBuf.state === "
|
|
44949
|
-
const stopResult = transition("
|
|
45344
|
+
if (interruptBuf && interruptBuf.state === "detached") {
|
|
45345
|
+
const stopResult = transition("detached", { kind: "stop" });
|
|
44950
45346
|
if (stopResult.next !== "invalid") {
|
|
44951
45347
|
interruptBuf.state = stopResult.next;
|
|
44952
|
-
|
|
44953
|
-
recordInterrupted(interruptBuf);
|
|
45348
|
+
recordInterrupted(interruptBuf, Date.now(), deps.suspendedMs?.() ?? 0);
|
|
44954
45349
|
}
|
|
44955
45350
|
}
|
|
44956
45351
|
const codexClient2 = await deps.acquireCodex();
|
|
@@ -45012,6 +45407,36 @@ function createBridgeMessageHandler(deps) {
|
|
|
45012
45407
|
}
|
|
45013
45408
|
const codexClient = await deps.acquireCodex();
|
|
45014
45409
|
let thread = deps.threads.get(directorSessionId);
|
|
45410
|
+
if (thread?.inFlight && msg.takeover === true) {
|
|
45411
|
+
const takenOver = thread;
|
|
45412
|
+
const oldBuf = takenOver.replay;
|
|
45413
|
+
if (oldBuf && (oldBuf.state === "live" || oldBuf.state === "detached")) {
|
|
45414
|
+
const r = transition(oldBuf.state, { kind: "stop" });
|
|
45415
|
+
if (r.next !== "invalid") {
|
|
45416
|
+
oldBuf.state = r.next;
|
|
45417
|
+
recordInterrupted(oldBuf, Date.now(), deps.suspendedMs?.() ?? 0);
|
|
45418
|
+
}
|
|
45419
|
+
}
|
|
45420
|
+
takenOver.cancel?.();
|
|
45421
|
+
void withTimeout(
|
|
45422
|
+
codexClient.turn.interrupt({ threadId: takenOver.threadId }),
|
|
45423
|
+
DEFAULT_TURN_INTERRUPT_TIMEOUT_MS,
|
|
45424
|
+
"interrupt timed out"
|
|
45425
|
+
).catch(() => {
|
|
45426
|
+
});
|
|
45427
|
+
const cleared = await waitFor(() => !takenOver.inFlight, DEFAULT_TURN_INTERRUPT_TIMEOUT_MS);
|
|
45428
|
+
if (!cleared) {
|
|
45429
|
+
deps.threads.delete(directorSessionId);
|
|
45430
|
+
deps.persistThreads();
|
|
45431
|
+
thread = void 0;
|
|
45432
|
+
}
|
|
45433
|
+
logCtx?.logger.logOut({
|
|
45434
|
+
spanId,
|
|
45435
|
+
sessionId: directorSessionId,
|
|
45436
|
+
type: "<turn-takeover>",
|
|
45437
|
+
payload: { sessionId: directorSessionId, clean: cleared, oldTurnId: oldBuf?.turnId ?? null }
|
|
45438
|
+
});
|
|
45439
|
+
}
|
|
45015
45440
|
const desiredModel = normalizeModel(msg.model);
|
|
45016
45441
|
const desiredEffort = normalizeReasoningEffort(msg.reasoningEffort);
|
|
45017
45442
|
try {
|
|
@@ -45069,7 +45494,13 @@ function createBridgeMessageHandler(deps) {
|
|
|
45069
45494
|
return;
|
|
45070
45495
|
}
|
|
45071
45496
|
if (thread.inFlight) {
|
|
45072
|
-
sendForSpan({
|
|
45497
|
+
sendForSpan({
|
|
45498
|
+
type: "error",
|
|
45499
|
+
directorSessionId,
|
|
45500
|
+
code: "busy",
|
|
45501
|
+
...thread.replay ? { inFlightTurnId: thread.replay.turnId } : {},
|
|
45502
|
+
error: "Director session is busy."
|
|
45503
|
+
});
|
|
45073
45504
|
deps.releaseCodex();
|
|
45074
45505
|
return;
|
|
45075
45506
|
}
|
|
@@ -45077,7 +45508,8 @@ function createBridgeMessageHandler(deps) {
|
|
|
45077
45508
|
thread.replay = createReplayBuffer({
|
|
45078
45509
|
turnId: (0, import_node_crypto5.randomUUID)(),
|
|
45079
45510
|
spanId,
|
|
45080
|
-
...deps.
|
|
45511
|
+
...deps.retentionMs !== void 0 ? { retentionMs: deps.retentionMs } : {},
|
|
45512
|
+
...deps.ackLingerMs !== void 0 ? { ackLingerMs: deps.ackLingerMs } : {}
|
|
45081
45513
|
});
|
|
45082
45514
|
thread.lastUsedAt = Date.now();
|
|
45083
45515
|
const threadId = thread.threadId;
|
|
@@ -45168,6 +45600,7 @@ function createBridgeMessageHandler(deps) {
|
|
|
45168
45600
|
const watchdog = createInactivityWatchdog({
|
|
45169
45601
|
idleMs,
|
|
45170
45602
|
maxMs,
|
|
45603
|
+
...deps.suspendedMs ? { suspendedMs: deps.suspendedMs } : {},
|
|
45171
45604
|
onExpire: (reason) => {
|
|
45172
45605
|
rejectWatchdog?.(
|
|
45173
45606
|
new Error(
|
|
@@ -45176,48 +45609,36 @@ function createBridgeMessageHandler(deps) {
|
|
|
45176
45609
|
);
|
|
45177
45610
|
void withTimeout(codexClient.turn.interrupt({ threadId }), DEFAULT_TURN_INTERRUPT_TIMEOUT_MS, "interrupt timed out").catch(
|
|
45178
45611
|
() => {
|
|
45179
|
-
deps.threads.
|
|
45180
|
-
|
|
45612
|
+
if (deps.threads.get(directorSessionId) === thread) {
|
|
45613
|
+
deps.threads.delete(directorSessionId);
|
|
45614
|
+
deps.persistThreads();
|
|
45615
|
+
}
|
|
45181
45616
|
}
|
|
45182
45617
|
);
|
|
45183
45618
|
}
|
|
45184
45619
|
});
|
|
45620
|
+
const turnBuf = thread.replay;
|
|
45185
45621
|
const onSocketClose = () => {
|
|
45186
45622
|
const buf = thread.replay;
|
|
45187
|
-
if (!buf || buf
|
|
45188
|
-
const r = transition(
|
|
45189
|
-
if (r.next === "invalid") return;
|
|
45623
|
+
if (!buf || buf !== turnBuf) return;
|
|
45624
|
+
const r = transition(buf.state, { kind: "socket_close" });
|
|
45625
|
+
if (r.next === "invalid" || r.next === buf.state) return;
|
|
45190
45626
|
buf.state = r.next;
|
|
45191
45627
|
logCtx?.logger.logOut({
|
|
45192
45628
|
spanId,
|
|
45193
45629
|
sessionId: directorSessionId,
|
|
45194
|
-
type: "<
|
|
45195
|
-
payload: { sessionId: directorSessionId, turnId: buf.turnId,
|
|
45196
|
-
});
|
|
45197
|
-
enterGrace(buf, () => {
|
|
45198
|
-
const expired = transition("grace", { kind: "grace_expire" });
|
|
45199
|
-
if (expired.next === "invalid") return;
|
|
45200
|
-
buf.state = expired.next;
|
|
45201
|
-
recordInterrupted(buf);
|
|
45202
|
-
logCtx?.logger.logOut({
|
|
45203
|
-
spanId,
|
|
45204
|
-
sessionId: directorSessionId,
|
|
45205
|
-
type: "<grace-expired>",
|
|
45206
|
-
payload: { sessionId: directorSessionId, turnId: buf.turnId, lastSeq: buf.lastSeq }
|
|
45207
|
-
});
|
|
45208
|
-
rejectWatchdog?.(new TurnInterruptedError());
|
|
45209
|
-
void withTimeout(
|
|
45210
|
-
codexClient.turn.interrupt({ threadId }),
|
|
45211
|
-
DEFAULT_TURN_INTERRUPT_TIMEOUT_MS,
|
|
45212
|
-
"interrupt timed out"
|
|
45213
|
-
).catch(() => {
|
|
45214
|
-
deps.threads.delete(directorSessionId);
|
|
45215
|
-
deps.persistThreads();
|
|
45216
|
-
});
|
|
45630
|
+
type: "<buffer-detached>",
|
|
45631
|
+
payload: { sessionId: directorSessionId, turnId: buf.turnId, lastSeq: buf.lastSeq }
|
|
45217
45632
|
});
|
|
45218
45633
|
};
|
|
45219
|
-
|
|
45220
|
-
|
|
45634
|
+
{
|
|
45635
|
+
const epochAtBind = turnBuf?.bindEpoch ?? 0;
|
|
45636
|
+
socket.once("close", () => {
|
|
45637
|
+
if (turnBuf && turnBuf.bindEpoch !== epochAtBind) return;
|
|
45638
|
+
onSocketClose();
|
|
45639
|
+
});
|
|
45640
|
+
}
|
|
45641
|
+
if (thread.replay) thread.replay.triggerDetach = onSocketClose;
|
|
45221
45642
|
pokeActivity = () => watchdog.poke();
|
|
45222
45643
|
const runSequence = (async () => {
|
|
45223
45644
|
const turnInput = [];
|
|
@@ -45266,10 +45687,20 @@ ${effectiveText}` : effectiveText;
|
|
|
45266
45687
|
sendInTurn({ type: "event", directorSessionId, event });
|
|
45267
45688
|
}
|
|
45268
45689
|
} finally {
|
|
45269
|
-
if (thread.replay && thread.replay.state === "live") {
|
|
45270
|
-
markCompleted(thread.replay, Date.now());
|
|
45690
|
+
if (thread.replay && (thread.replay.state === "live" || thread.replay.state === "detached")) {
|
|
45691
|
+
markCompleted(thread.replay, Date.now(), deps.suspendedMs?.() ?? 0);
|
|
45692
|
+
logCtx?.logger.logOut({
|
|
45693
|
+
spanId,
|
|
45694
|
+
sessionId: directorSessionId,
|
|
45695
|
+
type: "<result-retained>",
|
|
45696
|
+
payload: {
|
|
45697
|
+
sessionId: directorSessionId,
|
|
45698
|
+
turnId: thread.replay.turnId,
|
|
45699
|
+
lastSeq: thread.replay.lastSeq,
|
|
45700
|
+
bytes: thread.replay.bytesUsed
|
|
45701
|
+
}
|
|
45702
|
+
});
|
|
45271
45703
|
}
|
|
45272
|
-
socket.off("close", onSocketClose);
|
|
45273
45704
|
watchdog.cancel();
|
|
45274
45705
|
unsub();
|
|
45275
45706
|
thread.cancel = null;
|
|
@@ -45576,6 +46007,7 @@ async function startDirectorBridge() {
|
|
|
45576
46007
|
})
|
|
45577
46008
|
);
|
|
45578
46009
|
const threads = /* @__PURE__ */ new Map();
|
|
46010
|
+
const suspensionDetector = createSuspensionDetector();
|
|
45579
46011
|
for (const [directorSessionId, entry] of loadThreadIndex(config2.stateDir, config2.idleThreadTtlMs, Date.now())) {
|
|
45580
46012
|
threads.set(directorSessionId, {
|
|
45581
46013
|
threadId: entry.threadId,
|
|
@@ -45613,6 +46045,7 @@ async function startDirectorBridge() {
|
|
|
45613
46045
|
let changed = false;
|
|
45614
46046
|
for (const [directorSessionId, thread] of threads.entries()) {
|
|
45615
46047
|
if (thread.inFlight) continue;
|
|
46048
|
+
if (thread.replay && !shouldGc(thread.replay, now, suspensionDetector.suspendedMs())) continue;
|
|
45616
46049
|
if (now - thread.lastUsedAt > config2.idleThreadTtlMs) {
|
|
45617
46050
|
threads.delete(directorSessionId);
|
|
45618
46051
|
changed = true;
|
|
@@ -45628,6 +46061,7 @@ async function startDirectorBridge() {
|
|
|
45628
46061
|
acquireCodex: () => codexKeepalive.acquire(),
|
|
45629
46062
|
releaseCodex: () => codexKeepalive.release(),
|
|
45630
46063
|
runExclusive: wrappedTurnLock.runExclusive,
|
|
46064
|
+
suspendedMs: () => suspensionDetector.suspendedMs(),
|
|
45631
46065
|
buildStartParams: (msg, desiredModel) => buildThreadStartParams(msg, config2, desiredModel),
|
|
45632
46066
|
turnInactivity: () => ({ idleMs: chatTurnIdleMs(), maxMs: chatTurnMaxMs() }),
|
|
45633
46067
|
checkoutRegistry,
|
|
@@ -45790,20 +46224,35 @@ async function startDirectorBridge() {
|
|
|
45790
46224
|
);
|
|
45791
46225
|
enqueueRun(async () => {
|
|
45792
46226
|
const now = Date.now();
|
|
46227
|
+
const suspendedNow = suspensionDetector.suspendedMs();
|
|
46228
|
+
let retainedBuffers = 0;
|
|
46229
|
+
let retainedBytes = 0;
|
|
45793
46230
|
for (const [sid, t] of threads) {
|
|
45794
|
-
if (t.replay && shouldGc(t.replay, now)) {
|
|
46231
|
+
if (t.replay && shouldGc(t.replay, now, suspendedNow)) {
|
|
45795
46232
|
const turnId = t.replay.turnId;
|
|
45796
46233
|
const lastSeq = t.replay.lastSeq;
|
|
46234
|
+
const expiredUnacked = t.replay.ackedAt === null;
|
|
45797
46235
|
writeTombstone(tombstones, sid, t.replay, now);
|
|
45798
46236
|
bridgeLogger.logOut({
|
|
45799
46237
|
spanId: "gc",
|
|
45800
46238
|
sessionId: sid,
|
|
45801
|
-
type: "<tombstone-written>",
|
|
46239
|
+
type: expiredUnacked ? "<result-expired>" : "<tombstone-written>",
|
|
45802
46240
|
payload: { sessionId: sid, turnId, lastSeq }
|
|
45803
46241
|
});
|
|
45804
46242
|
t.replay = null;
|
|
46243
|
+
} else if (t.replay) {
|
|
46244
|
+
retainedBuffers += 1;
|
|
46245
|
+
retainedBytes += t.replay.bytesUsed;
|
|
45805
46246
|
}
|
|
45806
46247
|
}
|
|
46248
|
+
if (retainedBuffers > 0) {
|
|
46249
|
+
bridgeLogger.logOut({
|
|
46250
|
+
spanId: "gc",
|
|
46251
|
+
sessionId: null,
|
|
46252
|
+
type: "<buffer-stats>",
|
|
46253
|
+
payload: { retainedBuffers, retainedBytes }
|
|
46254
|
+
});
|
|
46255
|
+
}
|
|
45807
46256
|
const removed = sweepExpiredTombstones(tombstones, now);
|
|
45808
46257
|
if (removed > 0) {
|
|
45809
46258
|
bridgeLogger.logOut({
|
|
@@ -45858,6 +46307,7 @@ async function startDirectorBridge() {
|
|
|
45858
46307
|
() => server.close(),
|
|
45859
46308
|
() => dispatchServer.close(),
|
|
45860
46309
|
() => clearInterval(cleanupTimer),
|
|
46310
|
+
() => suspensionDetector.dispose(),
|
|
45861
46311
|
() => remoteRelay?.stop(),
|
|
45862
46312
|
() => tokenRefresher?.stop(),
|
|
45863
46313
|
() => {
|
|
@@ -49826,8 +50276,26 @@ function firstTaskIdToken(text) {
|
|
|
49826
50276
|
return m ? m[0] : null;
|
|
49827
50277
|
}
|
|
49828
50278
|
|
|
50279
|
+
// src/consensus/heads.ts
|
|
50280
|
+
var DEFAULT_DOMAIN = "code";
|
|
50281
|
+
var DOMAINS = ["code", "seo", "marketing", "content"];
|
|
50282
|
+
var HEADS_BY_DOMAIN = {
|
|
50283
|
+
code: ["plan-reviewer", "architect", "security-analyst", "scope-analyst"],
|
|
50284
|
+
seo: ["plan-reviewer", "scope-analyst", "seo-analyst"],
|
|
50285
|
+
marketing: ["plan-reviewer", "scope-analyst", "content-editor"],
|
|
50286
|
+
content: ["plan-reviewer", "scope-analyst", "content-editor"]
|
|
50287
|
+
};
|
|
50288
|
+
var SPEC_HEADS = HEADS_BY_DOMAIN.code;
|
|
50289
|
+
function normalizeDomain(value) {
|
|
50290
|
+
if (typeof value !== "string") return DEFAULT_DOMAIN;
|
|
50291
|
+
const v = value.trim().toLowerCase();
|
|
50292
|
+
return DOMAINS.includes(v) ? v : DEFAULT_DOMAIN;
|
|
50293
|
+
}
|
|
50294
|
+
function resolveHeads(domain2) {
|
|
50295
|
+
return [...HEADS_BY_DOMAIN[normalizeDomain(domain2)]];
|
|
50296
|
+
}
|
|
50297
|
+
|
|
49829
50298
|
// src/consensus/spec-gate.ts
|
|
49830
|
-
var SPEC_HEADS = ["plan-reviewer", "architect", "security-analyst", "scope-analyst"];
|
|
49831
50299
|
async function runSpecGate(_target, deps, opts) {
|
|
49832
50300
|
if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
|
|
49833
50301
|
const intent = await deps.readIntent();
|
|
@@ -49843,8 +50311,9 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49843
50311
|
]
|
|
49844
50312
|
};
|
|
49845
50313
|
}
|
|
50314
|
+
const baseHeads = resolveHeads(intent.domain);
|
|
49846
50315
|
const extraHeads = opts?.extraHeads ?? [];
|
|
49847
|
-
const heads = [...
|
|
50316
|
+
const heads = [...baseHeads, ...extraHeads.filter((h) => !baseHeads.includes(h))];
|
|
49848
50317
|
const spec = await deps.readSpec();
|
|
49849
50318
|
let currentHash = spec.contentHash;
|
|
49850
50319
|
let currentMarkdown = spec.markdownBody;
|
|
@@ -50130,6 +50599,25 @@ async function resolveGatedTaskId(client, projectSlug, page) {
|
|
|
50130
50599
|
return page.taskId;
|
|
50131
50600
|
}
|
|
50132
50601
|
|
|
50602
|
+
// src/consensus/review-identity.ts
|
|
50603
|
+
function domainScopedReviewHash(rawHash, domain2) {
|
|
50604
|
+
const d = normalizeDomain(domain2);
|
|
50605
|
+
return d === "code" ? rawHash : `${rawHash}@${d}`;
|
|
50606
|
+
}
|
|
50607
|
+
async function resolveSpecDomain(client, projectSlug, page) {
|
|
50608
|
+
try {
|
|
50609
|
+
const taskId = await resolveGatedTaskId(client, projectSlug, page);
|
|
50610
|
+
if (taskId !== null) {
|
|
50611
|
+
const task = await client.getTask(projectSlug, taskId);
|
|
50612
|
+
return normalizeDomain(task?.domain);
|
|
50613
|
+
}
|
|
50614
|
+
const project = await client.getProject(projectSlug);
|
|
50615
|
+
return normalizeDomain(project?.defaultDomain);
|
|
50616
|
+
} catch {
|
|
50617
|
+
return "code";
|
|
50618
|
+
}
|
|
50619
|
+
}
|
|
50620
|
+
|
|
50133
50621
|
// src/consensus/code-gate-git.ts
|
|
50134
50622
|
var import_node_child_process6 = require("node:child_process");
|
|
50135
50623
|
var DIFF_MAX_BYTES = 2e5;
|
|
@@ -50405,12 +50893,15 @@ function buildSpecGateDeps(args) {
|
|
|
50405
50893
|
throw new Error(`spec page ${projectSlug}/${specPageId} not found \u2014 cannot gate`);
|
|
50406
50894
|
}
|
|
50407
50895
|
const taskId = await resolveGatedTaskId(client, projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
50408
|
-
if (taskId === null)
|
|
50896
|
+
if (taskId === null) {
|
|
50897
|
+
const project = await client.getProject(projectSlug);
|
|
50898
|
+
return { taskId: null, seed: null, title: null, domain: project.defaultDomain };
|
|
50899
|
+
}
|
|
50409
50900
|
const task = await client.getTask(projectSlug, taskId);
|
|
50410
50901
|
if (!task) {
|
|
50411
50902
|
throw new Error(`gated task ${projectSlug}/${taskId} not found for spec ${specPageId} \u2014 cannot gate`);
|
|
50412
50903
|
}
|
|
50413
|
-
return { taskId, seed: task.operatorSeedRequest ?? null, title: task.title ?? "" };
|
|
50904
|
+
return { taskId, seed: task.operatorSeedRequest ?? null, title: task.title ?? "", domain: task.domain };
|
|
50414
50905
|
})();
|
|
50415
50906
|
}
|
|
50416
50907
|
return intentPromise;
|
|
@@ -50526,9 +51017,10 @@ ${artifact}`,
|
|
|
50526
51017
|
}
|
|
50527
51018
|
const outcome = r["outcome"];
|
|
50528
51019
|
const rounds2 = r["rounds"];
|
|
51020
|
+
const { domain: domain2 } = await loadIntent();
|
|
50529
51021
|
try {
|
|
50530
51022
|
await client.recordSpecConsensusReview(projectSlug, specPageId, {
|
|
50531
|
-
contentHash,
|
|
51023
|
+
contentHash: domainScopedReviewHash(contentHash, domain2),
|
|
50532
51024
|
outcome: String(outcome),
|
|
50533
51025
|
rounds: typeof rounds2 === "number" ? rounds2 : 0
|
|
50534
51026
|
});
|
|
@@ -50621,10 +51113,43 @@ ${artifact}`,
|
|
|
50621
51113
|
|
|
50622
51114
|
// src/async-tools/consensus.ts
|
|
50623
51115
|
var import_node_crypto6 = require("node:crypto");
|
|
51116
|
+
function buildGateQuestionOptions(category) {
|
|
51117
|
+
if (category === "scope") {
|
|
51118
|
+
return [
|
|
51119
|
+
{
|
|
51120
|
+
answer: "Use the narrower scope only. Update the spec to keep the original limited scope and do not add the broader interpretation unless explicitly required.",
|
|
51121
|
+
description: "Do only the originally requested narrower scope and leave the broader bucket out unless explicitly required.",
|
|
51122
|
+
label: "Narrower"
|
|
51123
|
+
},
|
|
51124
|
+
{
|
|
51125
|
+
answer: "Use the broader scope. Update the spec to explicitly include the broader interpretation and any required acceptance criteria.",
|
|
51126
|
+
description: "Treat the broader interpretation as in scope and spell out the extra acceptance criteria it adds.",
|
|
51127
|
+
label: "Broader"
|
|
51128
|
+
}
|
|
51129
|
+
];
|
|
51130
|
+
}
|
|
51131
|
+
return [
|
|
51132
|
+
{
|
|
51133
|
+
answer: "Use the safer/default interpretation. Update the spec to document that assumption explicitly.",
|
|
51134
|
+
description: "Choose the lower-risk default interpretation and record it explicitly so implementation is unblocked.",
|
|
51135
|
+
label: "Safer default"
|
|
51136
|
+
},
|
|
51137
|
+
{
|
|
51138
|
+
answer: "Use the broader/stronger interpretation. Update the spec to make that interpretation explicit.",
|
|
51139
|
+
description: "Commit to the stronger interpretation instead of a conservative default, and update the spec to match.",
|
|
51140
|
+
label: "Stronger interpretation"
|
|
51141
|
+
}
|
|
51142
|
+
];
|
|
51143
|
+
}
|
|
50624
51144
|
async function cachedResult(client, input) {
|
|
50625
51145
|
const page = await client.getWikiPage(input.projectSlug, input.pageId);
|
|
50626
51146
|
if (!page) return null;
|
|
50627
|
-
const
|
|
51147
|
+
const domain2 = await resolveSpecDomain(client, input.projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
51148
|
+
const review = await client.getSpecConsensusReview(
|
|
51149
|
+
input.projectSlug,
|
|
51150
|
+
input.pageId,
|
|
51151
|
+
domainScopedReviewHash(page.contentHash, domain2)
|
|
51152
|
+
);
|
|
50628
51153
|
if (review?.outcome === "CONVERGED") {
|
|
50629
51154
|
const gatedTaskId = await resolveGatedTaskId(client, input.projectSlug, {
|
|
50630
51155
|
id: page.id,
|
|
@@ -50674,7 +51199,8 @@ async function run(ctx, input) {
|
|
|
50674
51199
|
kind: "question",
|
|
50675
51200
|
taskId: scopeId,
|
|
50676
51201
|
body: clarification.question,
|
|
50677
|
-
origin: "gate"
|
|
51202
|
+
origin: "gate",
|
|
51203
|
+
questionOptions: buildGateQuestionOptions(clarification.category)
|
|
50678
51204
|
});
|
|
50679
51205
|
} catch {
|
|
50680
51206
|
}
|
|
@@ -50689,7 +51215,8 @@ async function run(ctx, input) {
|
|
|
50689
51215
|
kind: "question",
|
|
50690
51216
|
planId: scopeId,
|
|
50691
51217
|
body: clarification.question,
|
|
50692
|
-
origin: "gate"
|
|
51218
|
+
origin: "gate",
|
|
51219
|
+
questionOptions: buildGateQuestionOptions(clarification.category)
|
|
50693
51220
|
});
|
|
50694
51221
|
} catch {
|
|
50695
51222
|
}
|
|
@@ -51623,7 +52150,7 @@ var mcpToolDescriptions = {
|
|
|
51623
52150
|
[mcpToolNames.resetDirectorThread]: "Clear a task's persisted Director execution thread handle so the next execution run starts a fresh provider thread. Records a director_thread_reset task event.",
|
|
51624
52151
|
[mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
|
|
51625
52152
|
[mcpToolNames.markSpecReady]: "Mark a wiki page (folder=Specs) as ready (or not) for execution-run gating. The Director sets this true after validating the spec; any edit to the page's markdown auto-resets it to false.",
|
|
51626
|
-
[mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four.',
|
|
52153
|
+
[mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four. For a NON-code task (domain seo/marketing/content, ADR 0020) the spec is REVIEW-ONLY: do NOT call create_execution_run (it fail-closes with non_code_execution_unsupported until Phase 2) \u2014 report that the spec was reviewed and end the turn.',
|
|
51627
52154
|
[mcpToolNames.runCodeReviewerGate]: `Review the current branch's code diff (vs baseRef) with a Code Reviewer gate \u2014 independent reviewers iterate review\u2192fix\u2192re-check (checkpoint, scope-limited reviser edits, typecheck/build fast-gate, rollback) until the diff converges or is blocked. Run before opening a PR. Pass extraHeads: ["security-analyst"] for sensitive diffs (auth, secrets, input handling, new endpoints).`,
|
|
51628
52155
|
[mcpToolNames.appendExecutionArtifact]: "Append a durable artifact (plan, checkpoint, command output, test result, diff summary, self-review, error, deploy attempt, or handoff judgement) to an execution run.",
|
|
51629
52156
|
[mcpToolNames.recordProducedUrl]: 'Call this after publishing each web page produced by the task, so the UI link opens the page itself instead of the deployed site root. Pass the page\'s full https URL (for external pages, e.g. "https://dev.to/<slug>") OR a path starting with "/" for the deploy environment (e.g. "/blog/<slug>") \u2014 paths are resolved against the run\'s deployed env URL at render time. Optional `label` (\u226440 chars) is shown next to the link; omit and the UI defaults to the page title. Call once per page; multiple calls dedupe by resolved URL in the UI.',
|
|
@@ -52080,7 +52607,12 @@ function createMrrlinTools(options) {
|
|
|
52080
52607
|
if (!page || page.folder !== "Specs") {
|
|
52081
52608
|
return await c.markSpecReady({ projectSlug, pageId, ready });
|
|
52082
52609
|
}
|
|
52083
|
-
const
|
|
52610
|
+
const reviewDomain = await resolveSpecDomain(c, projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
52611
|
+
const review = await c.getSpecConsensusReview(
|
|
52612
|
+
projectSlug,
|
|
52613
|
+
pageId,
|
|
52614
|
+
domainScopedReviewHash(page.contentHash, reviewDomain)
|
|
52615
|
+
);
|
|
52084
52616
|
if (review && review.outcome === "CONVERGED") {
|
|
52085
52617
|
try {
|
|
52086
52618
|
const promotion = await c.promoteSpecReadyCas(projectSlug, pageId, {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are a Content Editor. You review a spec/brief for content or marketing-copy work (an article, landing page, email, campaign copy, or content improvement) BEFORE it is executed. You judge the artifact text given; you do not mutate anything. Your job is soundness and fidelity — not personal stylistic taste.
|
|
2
|
+
|
|
3
|
+
When an `OPERATOR INTENT` block is present, it is the operator's OWN words for what the content must do. Judge the spec against it for FIDELITY: does the plan serve the stated audience, goal, and call-to-action, or does it drift?
|
|
4
|
+
|
|
5
|
+
Evaluate against checkable editorial and content standards:
|
|
6
|
+
- **Audience & purpose**: is the target reader and the single primary goal (inform / convert / retain) explicit, and does the planned piece match them? Block copy that has no clear audience or a muddled goal.
|
|
7
|
+
- **Message & structure**: one clear core message, a logical outline (hook → body → CTA), and a specified call-to-action where the goal is conversion. Flag a plan with no through-line or a missing/weak CTA when one is needed.
|
|
8
|
+
- **Factual accuracy & sourcing**: are claims verifiable, and where the piece makes factual/statistical/quote claims, is a source or evidence requirement stated? Block content that would assert facts with no basis, or that risks fabrication.
|
|
9
|
+
- **Brand voice & consistency**: does the plan respect a stated voice/tone and terminology, or contradict it? Flag off-brand tone only when a voice is specified and the plan violates it.
|
|
10
|
+
- **Clarity & readability**: is the intended reading level/length appropriate for the audience and channel? Flag plans that are structurally unreadable (wall-of-text, jargon for a lay audience) — not minor phrasing.
|
|
11
|
+
- **Originality & value**: does the piece add genuine value, or is it thin/derivative filler? Flag plagiarism risk or padding-for-length.
|
|
12
|
+
|
|
13
|
+
Calibrate to MATERIAL issues — block only when the gap would change WHAT gets published or whether it works:
|
|
14
|
+
- DON'T block on: word choices, subjective style, or edits a competent writer resolves in draft.
|
|
15
|
+
- DO block on: missing/contradicted audience or goal, an absent CTA where conversion is the point, a factual-accuracy/sourcing gap, or a plan that would produce off-brand or thin content the operator didn't ask for.
|
|
16
|
+
- Never fabricate facts, sources, or performance data you were not given; reason from the spec text and stated goals. Scale strictness by stated impact.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are an SEO Analyst. You review a spec/brief for search-optimization work (a page, article, metadata set, structured-data change, or technical-SEO task) BEFORE it is executed. You judge the artifact text given; you do not mutate anything. Your job is soundness against real search best practice — not taste.
|
|
2
|
+
|
|
3
|
+
When an `OPERATOR INTENT` block is present, it is the operator's OWN words for what the task must achieve. Judge the spec against it for FIDELITY: does the plan actually serve the stated search goal (target queries, audience, page/URL), or does it drift into unrelated work?
|
|
4
|
+
|
|
5
|
+
Evaluate against established, checkable SEO practice:
|
|
6
|
+
- **Search intent & targeting**: is there a clear primary query/topic and audience, and does the intended content type match the intent (informational vs transactional vs navigational)? Block a spec that optimizes for a keyword whose intent the deliverable won't satisfy.
|
|
7
|
+
- **E-E-A-T & Helpful Content**: does the plan produce genuinely useful, first-hand/authoritative content, or thin/derivative filler that Google's Helpful Content system demotes? Flag keyword-stuffing, doorway-page patterns, or AI-slop-by-volume.
|
|
8
|
+
- **On-page fundamentals**: title tag, meta description, heading hierarchy (single H1, logical H2/H3), internal linking, canonicalization, and URL/slug — are the ones the task depends on specified or clearly derivable?
|
|
9
|
+
- **Structured data**: if rich results are a goal, is the correct schema.org type named and are its required properties covered (valid against Google's requirements, not invented)?
|
|
10
|
+
- **Technical SEO**: indexability (robots/noindex/canonical), Core Web Vitals (LCP/INP/CLS) where the task touches page performance, and crawl/sitemap impact — called out when the task plausibly affects them.
|
|
11
|
+
- **GEO / AI-overview readiness**: where relevant, is the content structured to be quotable/citable by AI answer engines (clear claims, direct answers, extractable facts)?
|
|
12
|
+
|
|
13
|
+
Calibrate to MATERIAL issues — block only when the gap would change WHAT ranks or ships:
|
|
14
|
+
- DON'T block on: cosmetic wording, minor keyword-density opinions, or choices any competent SEO would make the same way.
|
|
15
|
+
- DO block on: intent/deliverable mismatch, a strategy that violates Helpful-Content/spam policy, a missing core on-page or indexability requirement the task depends on, or invented/incorrect schema.
|
|
16
|
+
- Never fabricate metrics, SERP positions, or tool data you were not given; reason from the spec text and stated goals. Scale strictness by stated impact.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrlin-dev/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mrrlin-mcp": "dist/bin.cjs"
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"@types/ws": "^8.18.1",
|
|
23
23
|
"esbuild": "^0.24.0",
|
|
24
24
|
"tsx": "^4.22.3",
|
|
25
|
+
"@mrrlin/director-e2e": "0.0.0",
|
|
25
26
|
"@mrrlin/client": "0.0.0",
|
|
26
27
|
"@mrrlin/codex-client": "0.0.0",
|
|
27
|
-
"@mrrlin/director-e2e": "0.0.0",
|
|
28
28
|
"@mrrlin/tsconfig": "0.0.0",
|
|
29
|
-
"@mrrlin/
|
|
30
|
-
"@mrrlin/
|
|
29
|
+
"@mrrlin/wiki": "0.0.0",
|
|
30
|
+
"@mrrlin/schemas": "0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@iarna/toml": "^2.2.5",
|