@mrrlin-dev/mcp 0.3.67 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +534 -93
- package/package.json +4 -4
package/dist/bin.cjs
CHANGED
|
@@ -20658,14 +20658,17 @@ var toml = __toESM(require_toml(), 1);
|
|
|
20658
20658
|
|
|
20659
20659
|
// src/api-base-url.ts
|
|
20660
20660
|
var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
|
|
20661
|
-
var STAGING_API_BASE_URL = "https://
|
|
20661
|
+
var STAGING_API_BASE_URL = "https://api-staging.mrrlin.com";
|
|
20662
20662
|
function normalizeUrl(value) {
|
|
20663
20663
|
return value.replace(/\/$/, "");
|
|
20664
20664
|
}
|
|
20665
20665
|
var RETIRED_API_BASE_URLS = {
|
|
20666
20666
|
// Retired 2026-07-28 when staging moved to the mrrlin2 Vercel team; serves a decommissioned
|
|
20667
20667
|
// Supabase project and now answers `GET /health` with 402 DEPLOYMENT_DISABLED. Added in 0.3.50.
|
|
20668
|
-
"mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL
|
|
20668
|
+
"mrrlin-api-fnnzzz-fnnzzzs-projects.vercel.app": STAGING_API_BASE_URL,
|
|
20669
|
+
// Retired 2026-08-14 when staging decoupled onto the stable custom domain api-staging.mrrlin.com.
|
|
20670
|
+
// A persisted operator configuration may still pin the mrrlin3 Vercel alias; retarget it in one hop.
|
|
20671
|
+
"mrrlin-api-mrrlin3.vercel.app": STAGING_API_BASE_URL
|
|
20669
20672
|
};
|
|
20670
20673
|
function isSameApiHost(a, b) {
|
|
20671
20674
|
const origin = (value) => {
|
|
@@ -20697,6 +20700,18 @@ function retargetRetiredApiBaseUrl(input) {
|
|
|
20697
20700
|
function retiredApiHostnames() {
|
|
20698
20701
|
return Object.keys(RETIRED_API_BASE_URLS);
|
|
20699
20702
|
}
|
|
20703
|
+
function bridgeUrlImpliesStaging(input) {
|
|
20704
|
+
const raw = input?.trim();
|
|
20705
|
+
if (!raw) return false;
|
|
20706
|
+
if (isLoopbackUrl(raw)) return true;
|
|
20707
|
+
let hostname3;
|
|
20708
|
+
try {
|
|
20709
|
+
hostname3 = new URL(raw).hostname.toLowerCase();
|
|
20710
|
+
} catch {
|
|
20711
|
+
return false;
|
|
20712
|
+
}
|
|
20713
|
+
return hostname3 === "staging.mrrlin.com";
|
|
20714
|
+
}
|
|
20700
20715
|
function isLoopbackUrl(input) {
|
|
20701
20716
|
try {
|
|
20702
20717
|
const url2 = new URL(input);
|
|
@@ -20843,11 +20858,12 @@ async function installBrowser(opts = {}) {
|
|
|
20843
20858
|
function deriveServeEnvFromProcessEnv(env) {
|
|
20844
20859
|
const out = {};
|
|
20845
20860
|
const rawApiUrl = env.MRRLIN_API_BASE_URL?.trim();
|
|
20861
|
+
const bridgeWantsStaging = bridgeUrlImpliesStaging(env.MRRLIN_BRIDGE_URL);
|
|
20846
20862
|
const apiUrl = rawApiUrl ? retargetRetiredApiBaseUrl(rawApiUrl).baseUrl : void 0;
|
|
20847
20863
|
if (apiUrl && !isStagingApiBaseUrl(apiUrl)) {
|
|
20848
20864
|
out.MRRLIN_API_BASE_URL = apiUrl;
|
|
20849
20865
|
if (env.MRRLIN_API_ALLOW_REMOTE === "1") out.MRRLIN_API_ALLOW_REMOTE = "1";
|
|
20850
|
-
} else if (apiUrl || env.MRRLIN_STAGING === "1") {
|
|
20866
|
+
} else if (apiUrl || env.MRRLIN_STAGING === "1" || bridgeWantsStaging) {
|
|
20851
20867
|
out.MRRLIN_STAGING = "1";
|
|
20852
20868
|
}
|
|
20853
20869
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
@@ -21006,7 +21022,7 @@ function blockEnvHasRetiredApiUrl(actual) {
|
|
|
21006
21022
|
}
|
|
21007
21023
|
function renderEnvPreservedNotice(blockId, envKeys) {
|
|
21008
21024
|
const keys = [...envKeys].sort().join(", ");
|
|
21009
|
-
return `[mrrlin-mcp
|
|
21025
|
+
return `[mrrlin-mcp configure bridge] kept your existing [mcp_servers.${blockId}] env keys (${keys}) so your API target is preserved. Re-run with --force to replace or remove it.`;
|
|
21010
21026
|
}
|
|
21011
21027
|
async function pathExists(target) {
|
|
21012
21028
|
try {
|
|
@@ -36387,6 +36403,9 @@ var openApiOperationIds = {
|
|
|
36387
36403
|
getExecutionRun: "getExecutionRun",
|
|
36388
36404
|
getProjectSchedulerPolicy: "getProjectSchedulerPolicy",
|
|
36389
36405
|
getProjectDeployWorkflow: "getProjectDeployWorkflow",
|
|
36406
|
+
getDeployTopology: "getDeployTopology",
|
|
36407
|
+
writeDeployTopology: "writeDeployTopology",
|
|
36408
|
+
getTaskDeployTarget: "getTaskDeployTarget",
|
|
36390
36409
|
getSchedulerPendingTasks: "getSchedulerPendingTasks",
|
|
36391
36410
|
wakeDirector: "wakeDirector",
|
|
36392
36411
|
getGithubInstallationRepositories: "getGithubInstallationRepositories",
|
|
@@ -36395,6 +36414,8 @@ var openApiOperationIds = {
|
|
|
36395
36414
|
getTask: "getTask",
|
|
36396
36415
|
archiveTask: "archiveTask",
|
|
36397
36416
|
unarchiveTask: "unarchiveTask",
|
|
36417
|
+
takeOverTask: "takeOverTask",
|
|
36418
|
+
closeTaskAsWontDo: "closeTaskAsWontDo",
|
|
36398
36419
|
getWikiPage: "getWikiPage",
|
|
36399
36420
|
handleGithubInstallCallback: "handleGithubInstallCallback",
|
|
36400
36421
|
listExecutionArtifacts: "listExecutionArtifacts",
|
|
@@ -36883,6 +36904,32 @@ var SUPPORTED_EXECUTION_DOMAINS = Object.keys(SUPPORTED_EXECUTION_POLICY_BY_DOMA
|
|
|
36883
36904
|
var taxonomyCategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
|
|
36884
36905
|
var taxonomySubcategorySchema = external_exports.string().min(1).regex(/^[a-z][a-z0-9-]*$/);
|
|
36885
36906
|
var mrrlinTaskAssigneeSchema = external_exports.union([external_exports.string().regex(/^@/), external_exports.string().email()]).nullable();
|
|
36907
|
+
var mrrlinStopReasonSchema = external_exports.enum([
|
|
36908
|
+
"task_terminal",
|
|
36909
|
+
"active_run_exists",
|
|
36910
|
+
"draft_running",
|
|
36911
|
+
"consensus_running",
|
|
36912
|
+
"spec_changed_during_advance",
|
|
36913
|
+
"run_limit_reached",
|
|
36914
|
+
"backoff_pending",
|
|
36915
|
+
"blocked_by_active",
|
|
36916
|
+
"spec_missing",
|
|
36917
|
+
"spec_draft_failed",
|
|
36918
|
+
"spec_not_specs_folder",
|
|
36919
|
+
"awaiting_clarification",
|
|
36920
|
+
"consensus_blocked",
|
|
36921
|
+
"consensus_run_failed",
|
|
36922
|
+
"consensus_disabled_spec_not_ready",
|
|
36923
|
+
"spec_approval_pending",
|
|
36924
|
+
"awaiting_handoff",
|
|
36925
|
+
"assigned_to_other",
|
|
36926
|
+
"attempts_exhausted",
|
|
36927
|
+
"daily_job_budget_exhausted",
|
|
36928
|
+
"taken_over",
|
|
36929
|
+
"operator_absent",
|
|
36930
|
+
"autonomy_paused",
|
|
36931
|
+
"unknown"
|
|
36932
|
+
]);
|
|
36886
36933
|
var mrrlinTaskSchema = external_exports.object({
|
|
36887
36934
|
// Output-optional (not defaulted-required) so existing task fixtures/mocks across the
|
|
36888
36935
|
// codebase need no churn; DB rows always resolve it to `string | null` via taskRowToTask.
|
|
@@ -36897,6 +36944,9 @@ var mrrlinTaskSchema = external_exports.object({
|
|
|
36897
36944
|
autonomyReason: external_exports.string().min(1).max(240),
|
|
36898
36945
|
blockedBy: external_exports.array(mrrlinTaskIdSchema),
|
|
36899
36946
|
category: taxonomyCategorySchema,
|
|
36947
|
+
// Slice 3 (task-stall-visibility): why a task was closed as won't-do -- populated only by
|
|
36948
|
+
// closeTaskAsWontDo. Output-optional so existing task fixtures need no churn.
|
|
36949
|
+
closeReason: external_exports.string().nullable().optional(),
|
|
36900
36950
|
directorThreadId: external_exports.string().nullable().default(null),
|
|
36901
36951
|
dueDate: external_exports.string().date().nullable(),
|
|
36902
36952
|
followUpAt: external_exports.string().datetime().nullable(),
|
|
@@ -36905,6 +36955,9 @@ var mrrlinTaskSchema = external_exports.object({
|
|
|
36905
36955
|
latestJudgement: external_exports.string().nullable().default(null),
|
|
36906
36956
|
latestJudgementAt: external_exports.string().datetime().nullable().default(null),
|
|
36907
36957
|
latestJudgementKind: external_exports.enum(["autonomy", "handoff"]).nullable().default(null),
|
|
36958
|
+
// Slice 3: the retry-backoff timer for exactly spec_draft_failed/consensus_run_failed -- a task
|
|
36959
|
+
// whose next_attempt_at is in the future is skipped from admission (backoff_pending).
|
|
36960
|
+
nextAttemptAt: external_exports.string().datetime().nullable().optional(),
|
|
36908
36961
|
notes: external_exports.string().nullable().default(null),
|
|
36909
36962
|
// Raw operator request captured at task creation (operator-authored ONLY, never Director-generated).
|
|
36910
36963
|
// The intent-aware spec gate judges the spec against this; null/absent ⇒ the gate fail-closes.
|
|
@@ -36914,6 +36967,14 @@ var mrrlinTaskSchema = external_exports.object({
|
|
|
36914
36967
|
planId: mrrlinPlanIdSchema.nullable(),
|
|
36915
36968
|
specApproved: mrrlinSpecApprovedSchema.optional(),
|
|
36916
36969
|
specWikiPageId: mrrlinWikiPageIdSchema.nullable(),
|
|
36970
|
+
// Slice 3: episode bookkeeping alongside the async_jobs-derived countEpisodeAttempts --
|
|
36971
|
+
// stallAttemptCount backs the exponential-backoff formula, stallRearmed guards against
|
|
36972
|
+
// re-arming the same episode's ceiling more than once.
|
|
36973
|
+
stallAttemptCount: external_exports.number().int().nonnegative().optional(),
|
|
36974
|
+
stallRearmed: external_exports.boolean().optional(),
|
|
36975
|
+
stallNextAction: external_exports.string().nullable().optional(),
|
|
36976
|
+
stallReason: mrrlinStopReasonSchema.nullable().optional(),
|
|
36977
|
+
stallSince: external_exports.string().datetime().nullable().optional(),
|
|
36917
36978
|
status: mrrlinTaskStatusSchema,
|
|
36918
36979
|
subcategory: taxonomySubcategorySchema,
|
|
36919
36980
|
tags: mrrlinTaskTagsSchema,
|
|
@@ -36976,6 +37037,12 @@ var mrrlinWikiPageSchema = external_exports.object({
|
|
|
36976
37037
|
var mrrlinMarkSpecReadyRequestSchema = external_exports.object({
|
|
36977
37038
|
ready: external_exports.boolean()
|
|
36978
37039
|
});
|
|
37040
|
+
var mrrlinTakeOverTaskRequestSchema = external_exports.object({
|
|
37041
|
+
assignee: external_exports.string().min(1)
|
|
37042
|
+
});
|
|
37043
|
+
var mrrlinCloseTaskAsWontDoRequestSchema = external_exports.object({
|
|
37044
|
+
reason: external_exports.string().min(1)
|
|
37045
|
+
});
|
|
36979
37046
|
var mrrlinGithubBindingSchema = external_exports.object({
|
|
36980
37047
|
appInstallationState: external_exports.enum(["installed", "missing"]),
|
|
36981
37048
|
installationId: external_exports.number().int().positive().nullable().default(null),
|
|
@@ -37133,6 +37200,47 @@ function refineTaskCreateDomain(data, ctx) {
|
|
|
37133
37200
|
}
|
|
37134
37201
|
var mrrlinTaskCreateSchema = mrrlinTaskCreateShapeSchema.superRefine(refineTaskCreateDomain);
|
|
37135
37202
|
var mrrlinTaskCreateWithoutIdSchema = mrrlinTaskCreateShapeSchema.omit({ id: true }).superRefine(refineTaskCreateDomain);
|
|
37203
|
+
var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
|
|
37204
|
+
var mrrlinTaskDeployTargetInputSchema = external_exports.object({
|
|
37205
|
+
env: mrrlinDeployWorkflowEnvSchema,
|
|
37206
|
+
targetLabel: external_exports.string().min(1),
|
|
37207
|
+
workflowId: external_exports.number().int().positive(),
|
|
37208
|
+
workflowPath: external_exports.string().min(1),
|
|
37209
|
+
workflowSha: external_exports.string().min(1),
|
|
37210
|
+
topologyFingerprint: external_exports.string().min(1)
|
|
37211
|
+
});
|
|
37212
|
+
var deployTopologyResponseSchema = external_exports.object({
|
|
37213
|
+
data: external_exports.object({
|
|
37214
|
+
fingerprint: external_exports.string().nullable(),
|
|
37215
|
+
snapshot: external_exports.array(external_exports.object({ id: external_exports.number(), path: external_exports.string(), sha: external_exports.string(), state: external_exports.string() })).nullable(),
|
|
37216
|
+
entries: external_exports.array(external_exports.object({
|
|
37217
|
+
label: external_exports.string(),
|
|
37218
|
+
env: mrrlinDeployWorkflowEnvSchema,
|
|
37219
|
+
workflowId: external_exports.number(),
|
|
37220
|
+
workflowPath: external_exports.string(),
|
|
37221
|
+
workflowSha: external_exports.string()
|
|
37222
|
+
}))
|
|
37223
|
+
})
|
|
37224
|
+
});
|
|
37225
|
+
var mrrlinDeployTopologyWriteSchema = external_exports.object({
|
|
37226
|
+
fingerprint: external_exports.string().min(1),
|
|
37227
|
+
entries: external_exports.array(external_exports.object({
|
|
37228
|
+
label: external_exports.string().min(1),
|
|
37229
|
+
env: mrrlinDeployWorkflowEnvSchema,
|
|
37230
|
+
workflowId: external_exports.number().int().positive(),
|
|
37231
|
+
workflowPath: external_exports.string().min(1),
|
|
37232
|
+
workflowSha: external_exports.string().min(1)
|
|
37233
|
+
}))
|
|
37234
|
+
});
|
|
37235
|
+
var taskDeployTargetResponseSchema = external_exports.object({
|
|
37236
|
+
data: external_exports.object({
|
|
37237
|
+
targetLabel: external_exports.string(),
|
|
37238
|
+
workflowId: external_exports.number(),
|
|
37239
|
+
workflowPath: external_exports.string(),
|
|
37240
|
+
workflowSha: external_exports.string(),
|
|
37241
|
+
topologyFingerprint: external_exports.string()
|
|
37242
|
+
})
|
|
37243
|
+
});
|
|
37136
37244
|
var mrrlinTaskUpdateSchema = external_exports.object({
|
|
37137
37245
|
actor: external_exports.string().min(1).optional(),
|
|
37138
37246
|
assignee: mrrlinTaskAssigneeSchema.optional(),
|
|
@@ -37155,8 +37263,13 @@ var mrrlinTaskUpdateSchema = external_exports.object({
|
|
|
37155
37263
|
title: external_exports.string().min(1).optional(),
|
|
37156
37264
|
type: mrrlinTaskTypeSchema.optional(),
|
|
37157
37265
|
followUpAt: external_exports.string().datetime().nullable().optional(),
|
|
37158
|
-
verificationDueAt: external_exports.string().datetime().nullable().optional()
|
|
37159
|
-
|
|
37266
|
+
verificationDueAt: external_exports.string().datetime().nullable().optional(),
|
|
37267
|
+
deployTargets: external_exports.array(mrrlinTaskDeployTargetInputSchema).optional(),
|
|
37268
|
+
// design.md D5 step 6 hint (task 3.16): the deploy gate's own bookkeeping write when the
|
|
37269
|
+
// fingerprint moved but the topology still maps the label to the same workflow. Never sent
|
|
37270
|
+
// by any other caller.
|
|
37271
|
+
reverifyRequested: external_exports.boolean().optional()
|
|
37272
|
+
}).refine((value) => value.assignee !== void 0 || value.autoDeploy !== void 0 || value.domain !== void 0 || value.autonomyLevel !== void 0 || value.autonomyReason !== void 0 || value.category !== void 0 || value.directorThreadId !== void 0 || value.dueDate !== void 0 || value.followUpAt !== void 0 || value.impact !== void 0 || value.notes !== void 0 || value.planId !== void 0 || value.specApproved !== void 0 || value.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.title !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0 || value.deployTargets !== void 0 || value.reverifyRequested !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
|
|
37160
37273
|
if (value.autonomyLevel !== void 0 && value.autonomyReason === void 0) {
|
|
37161
37274
|
context.addIssue({
|
|
37162
37275
|
code: external_exports.ZodIssueCode.custom,
|
|
@@ -37240,6 +37353,7 @@ var mrrlinWikiPageUpdateSchema = external_exports.object({
|
|
|
37240
37353
|
}).refine((value) => Object.keys(value).length > 0, { message: "WikiPageUpdate requires at least one field" });
|
|
37241
37354
|
var mrrlinWikiPageListFiltersSchema = external_exports.object({
|
|
37242
37355
|
folder: external_exports.string().min(1).optional(),
|
|
37356
|
+
id: mrrlinWikiPageIdSchema.optional(),
|
|
37243
37357
|
q: external_exports.string().min(1).optional(),
|
|
37244
37358
|
taskId: mrrlinTaskIdSchema.optional()
|
|
37245
37359
|
});
|
|
@@ -37816,7 +37930,6 @@ var mrrlinProjectSchedulerPolicyUpsertSchema = external_exports.object({
|
|
|
37816
37930
|
var projectSchedulerPolicyResponseSchema = external_exports.object({
|
|
37817
37931
|
data: mrrlinProjectSchedulerPolicySchema
|
|
37818
37932
|
});
|
|
37819
|
-
var mrrlinDeployWorkflowEnvSchema = external_exports.enum(["dev", "prod"]);
|
|
37820
37933
|
var mrrlinDeployWorkflowSourceSchema = external_exports.enum(["agent", "operator"]);
|
|
37821
37934
|
var mrrlinDeployWorkflowStateSchema = external_exports.enum(["resolved", "ambiguous"]);
|
|
37822
37935
|
var mrrlinProjectDeployWorkflowSchema = external_exports.object({
|
|
@@ -37945,7 +38058,8 @@ var mrrlinInFlightSchedulerEntrySchema = external_exports.discriminatedUnion("ki
|
|
|
37945
38058
|
]);
|
|
37946
38059
|
var mrrlinIneligibleTaskSchema = external_exports.object({
|
|
37947
38060
|
task: mrrlinTaskSchema,
|
|
37948
|
-
autonomyLevel: mrrlinAutonomyLevelSchema
|
|
38061
|
+
autonomyLevel: mrrlinAutonomyLevelSchema,
|
|
38062
|
+
reason: external_exports.string().nullable().optional()
|
|
37949
38063
|
});
|
|
37950
38064
|
var schedulerPendingTasksResponseSchema = external_exports.object({
|
|
37951
38065
|
data: external_exports.object({
|
|
@@ -37969,6 +38083,12 @@ var schedulerPendingTasksResponseSchema = external_exports.object({
|
|
|
37969
38083
|
// person actually running the project) is visible as a large number instead of the autopilot
|
|
37970
38084
|
// silently doing nothing.
|
|
37971
38085
|
excludedByOwnershipCount: external_exports.number().int().min(0),
|
|
38086
|
+
// Additive: the same ownership exclusion grouped by assignee. This reveals who the
|
|
38087
|
+
// autopilot is waiting for without returning task ids or titles in this endpoint.
|
|
38088
|
+
ownershipExcludedAssignees: external_exports.array(external_exports.object({
|
|
38089
|
+
assignee: external_exports.string(),
|
|
38090
|
+
count: external_exports.number().int().min(1)
|
|
38091
|
+
})),
|
|
37972
38092
|
// Additive: true when the operator-web-presence gate is the reason nothing new is starting
|
|
37973
38093
|
// (autonomyPaused takes precedence when both apply). Computed by the same shared helper the
|
|
37974
38094
|
// tick uses, so this page can never claim a task is starting when the tick would decline it.
|
|
@@ -38072,6 +38192,7 @@ var mrrlinInboxItemSchema = external_exports.object({
|
|
|
38072
38192
|
projectSlug: mrrlinProjectSlugSchema,
|
|
38073
38193
|
questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().default(null),
|
|
38074
38194
|
readAt: external_exports.string().datetime().nullable(),
|
|
38195
|
+
reason: external_exports.string().nullable().optional(),
|
|
38075
38196
|
status: mrrlinInboxItemStatusSchema,
|
|
38076
38197
|
taskId: mrrlinTaskIdSchema.nullable()
|
|
38077
38198
|
}).superRefine((value, context) => {
|
|
@@ -38918,6 +39039,22 @@ function createMrrlinClient(config2) {
|
|
|
38918
39039
|
});
|
|
38919
39040
|
return taskResponseSchema.parse(body).data;
|
|
38920
39041
|
},
|
|
39042
|
+
async takeOverTask(projectSlug, taskId, assignee) {
|
|
39043
|
+
const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/take-over`, {
|
|
39044
|
+
method: "POST",
|
|
39045
|
+
headers: { "content-type": "application/json" },
|
|
39046
|
+
body: JSON.stringify({ assignee })
|
|
39047
|
+
});
|
|
39048
|
+
return taskResponseSchema.parse(body).data;
|
|
39049
|
+
},
|
|
39050
|
+
async closeTaskAsWontDo(projectSlug, taskId, reason) {
|
|
39051
|
+
const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/wont-do`, {
|
|
39052
|
+
method: "POST",
|
|
39053
|
+
headers: { "content-type": "application/json" },
|
|
39054
|
+
body: JSON.stringify({ reason })
|
|
39055
|
+
});
|
|
39056
|
+
return taskResponseSchema.parse(body).data;
|
|
39057
|
+
},
|
|
38921
39058
|
async updateWikiPage(projectSlug, pageId, patch) {
|
|
38922
39059
|
const body = await request(`${projectPath(projectSlug)}/wiki/${encodeURIComponent(pageId)}`, {
|
|
38923
39060
|
body: JSON.stringify(patch),
|
|
@@ -39301,6 +39438,29 @@ function createMrrlinClient(config2) {
|
|
|
39301
39438
|
throw error51;
|
|
39302
39439
|
}
|
|
39303
39440
|
},
|
|
39441
|
+
async getDeployTopology(projectSlug) {
|
|
39442
|
+
const body = await request(`${projectPath(projectSlug)}/deploy-topology`);
|
|
39443
|
+
return deployTopologyResponseSchema.parse(body).data;
|
|
39444
|
+
},
|
|
39445
|
+
async writeDeployTopology(projectSlug, input) {
|
|
39446
|
+
const body = await request(`${projectPath(projectSlug)}/deploy-topology`, {
|
|
39447
|
+
body: JSON.stringify(mrrlinDeployTopologyWriteSchema.parse(input)),
|
|
39448
|
+
headers: { "content-type": "application/json" },
|
|
39449
|
+
method: "PUT"
|
|
39450
|
+
});
|
|
39451
|
+
return deployTopologyResponseSchema.parse(body).data;
|
|
39452
|
+
},
|
|
39453
|
+
async getTaskDeployTarget(projectSlug, taskId, env) {
|
|
39454
|
+
try {
|
|
39455
|
+
const body = await request(`${projectPath(projectSlug)}/tasks/${taskId}/deploy-target/${env}`);
|
|
39456
|
+
return taskDeployTargetResponseSchema.parse(body).data;
|
|
39457
|
+
} catch (error51) {
|
|
39458
|
+
if (error51 instanceof MrrlinApiError && error51.status === 404 && error51.code === "DEPLOY_TARGET_UNSET") {
|
|
39459
|
+
return null;
|
|
39460
|
+
}
|
|
39461
|
+
throw error51;
|
|
39462
|
+
}
|
|
39463
|
+
},
|
|
39304
39464
|
async upsertProjectDeployWorkflow(projectSlug, env, input) {
|
|
39305
39465
|
const body = await request(`${projectPath(projectSlug)}/deploy-workflows/${env}`, {
|
|
39306
39466
|
body: JSON.stringify(input),
|
|
@@ -39975,7 +40135,7 @@ function startCodexProbe(deps = {}) {
|
|
|
39975
40135
|
}
|
|
39976
40136
|
|
|
39977
40137
|
// src/_generated/version.ts
|
|
39978
|
-
var PKG_VERSION = "0.
|
|
40138
|
+
var PKG_VERSION = "0.4.0";
|
|
39979
40139
|
|
|
39980
40140
|
// src/api-base-url-migrate.ts
|
|
39981
40141
|
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
@@ -42355,6 +42515,111 @@ ${body}`;
|
|
|
42355
42515
|
}
|
|
42356
42516
|
return { markdown: body, tldrChars };
|
|
42357
42517
|
}
|
|
42518
|
+
function extractDeployTarget(markdown) {
|
|
42519
|
+
const section = /(## Deploy Target\s*\n)([\s\S]*?)(?=\n## |$)/i.exec(markdown);
|
|
42520
|
+
if (!section || section.index === void 0) {
|
|
42521
|
+
return { markdown, deployTarget: null };
|
|
42522
|
+
}
|
|
42523
|
+
const remaining = markdown.slice(0, section.index) + markdown.slice(section.index + section[0].length);
|
|
42524
|
+
const fenced = /```(?:json)?\s*\n([\s\S]*?)```/m.exec(section[2] ?? "");
|
|
42525
|
+
let deployTarget = null;
|
|
42526
|
+
if (fenced) {
|
|
42527
|
+
try {
|
|
42528
|
+
const raw = JSON.parse(fenced[1] ?? "");
|
|
42529
|
+
if (raw !== null && typeof raw === "object" && typeof raw.label === "string") {
|
|
42530
|
+
const parsed = { label: raw.label };
|
|
42531
|
+
if (Array.isArray(raw.derive)) {
|
|
42532
|
+
const derive = raw.derive.filter(isDeployTopologyEntry);
|
|
42533
|
+
if (derive.length === raw.derive.length) parsed.derive = derive;
|
|
42534
|
+
}
|
|
42535
|
+
deployTarget = parsed;
|
|
42536
|
+
}
|
|
42537
|
+
} catch {
|
|
42538
|
+
deployTarget = null;
|
|
42539
|
+
}
|
|
42540
|
+
}
|
|
42541
|
+
return { markdown: remaining, deployTarget };
|
|
42542
|
+
}
|
|
42543
|
+
function isDeployTopologyEntry(value) {
|
|
42544
|
+
if (value === null || typeof value !== "object") return false;
|
|
42545
|
+
const e = value;
|
|
42546
|
+
return typeof e.label === "string" && e.label.length > 0 && (e.env === "dev" || e.env === "prod") && typeof e.workflowId === "number" && Number.isInteger(e.workflowId) && e.workflowId > 0 && typeof e.workflowPath === "string" && e.workflowPath.length > 0 && typeof e.workflowSha === "string" && e.workflowSha.length > 0;
|
|
42547
|
+
}
|
|
42548
|
+
function buildDeployTargetPromptSection(ctx) {
|
|
42549
|
+
if (ctx.kind === "closed_menu") {
|
|
42550
|
+
const labels = [...new Set(ctx.entries.map((entry) => entry.label))];
|
|
42551
|
+
return [
|
|
42552
|
+
``,
|
|
42553
|
+
`--- DEPLOY TARGET (required final section) ---`,
|
|
42554
|
+
`This project's deploy topology is already resolved for its current workflow inventory. Pick the ONE label this task deploys under from this CLOSED menu \u2014 never invent a new label:`,
|
|
42555
|
+
...labels.map((label) => `- ${label}`),
|
|
42556
|
+
``,
|
|
42557
|
+
`Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
|
|
42558
|
+
"```json",
|
|
42559
|
+
`{"label": "<one of the menu labels above>"}`,
|
|
42560
|
+
"```"
|
|
42561
|
+
].join("\n");
|
|
42562
|
+
}
|
|
42563
|
+
return [
|
|
42564
|
+
``,
|
|
42565
|
+
`--- DEPLOY TARGET (required final section) ---`,
|
|
42566
|
+
`This project's deploy topology is NOT yet resolved for its current workflow inventory. Below is the raw GitHub workflow list (workflowId / path / blob sha / state); every value used below must be copied VERBATIM from it \u2014 never invented:`,
|
|
42567
|
+
``,
|
|
42568
|
+
JSON.stringify(ctx.snapshot, null, 2),
|
|
42569
|
+
``,
|
|
42570
|
+
`First derive the FULL label map: identify every deploy target in the snapshot, give each a short kebab-case \`label\`, and classify each workflow into an \`env\` of exactly "dev" or "prod".`,
|
|
42571
|
+
`Heuristic for dev vs prod (there is no fixed naming rule): prefer an explicit \`dev-\`/\`prod-\` filename prefix, then a \`/dev/\` or \`/prod/\` directory segment; if neither applies, group the workflows serving the same target and classify from the pair's context.`,
|
|
42572
|
+
`Then pick the ONE \`label\` this task deploys under, from among the labels you just derived.`,
|
|
42573
|
+
``,
|
|
42574
|
+
`Append a final section \`## Deploy Target\` whose ONLY content is a single fenced json block of exactly this shape:`,
|
|
42575
|
+
"```json",
|
|
42576
|
+
`{"label": "<label this task deploys under>", "derive": [{"label":"<target label>","env":"dev"|"prod","workflowId":N,"workflowPath":"<path>","workflowSha":"<blob sha>"}, ...]}`,
|
|
42577
|
+
"```",
|
|
42578
|
+
``,
|
|
42579
|
+
`\`derive\` is the FULL map of every deploy target you can identify in the snapshot (not just this task's pick), so later tasks reuse this derivation instead of re-deriving. \`env\` must be exactly "dev" or "prod". \`workflowId\`/\`workflowPath\`/\`workflowSha\` must be copied verbatim from the snapshot above.`
|
|
42580
|
+
].join("\n");
|
|
42581
|
+
}
|
|
42582
|
+
function resolveDeployTargetRows(canonical, deployTarget, autoDeploy, fingerprint) {
|
|
42583
|
+
if (autoDeploy === "off") return {};
|
|
42584
|
+
const filtered = canonical.filter((entry) => entry.label === deployTarget.label);
|
|
42585
|
+
const requiredEnvs = autoDeploy === "prod" ? ["dev", "prod"] : ["dev"];
|
|
42586
|
+
const missingEnvs = [];
|
|
42587
|
+
const deployTargets = [];
|
|
42588
|
+
for (const env of requiredEnvs) {
|
|
42589
|
+
const match = filtered.find((entry) => entry.env === env);
|
|
42590
|
+
if (!match) {
|
|
42591
|
+
missingEnvs.push(env);
|
|
42592
|
+
continue;
|
|
42593
|
+
}
|
|
42594
|
+
deployTargets.push({
|
|
42595
|
+
env,
|
|
42596
|
+
targetLabel: deployTarget.label,
|
|
42597
|
+
workflowId: match.workflowId,
|
|
42598
|
+
workflowPath: match.workflowPath,
|
|
42599
|
+
workflowSha: match.workflowSha,
|
|
42600
|
+
topologyFingerprint: fingerprint
|
|
42601
|
+
});
|
|
42602
|
+
}
|
|
42603
|
+
if (missingEnvs.length > 0) return { skippedReason: "incomplete_label", missingEnvs };
|
|
42604
|
+
return { deployTargets };
|
|
42605
|
+
}
|
|
42606
|
+
function findDuplicateLabelEnvPairs(entries) {
|
|
42607
|
+
const seen = /* @__PURE__ */ new Set();
|
|
42608
|
+
const duplicateKeys = /* @__PURE__ */ new Set();
|
|
42609
|
+
for (const entry of entries) {
|
|
42610
|
+
const key = `${entry.label}\0${entry.env}`;
|
|
42611
|
+
if (seen.has(key)) duplicateKeys.add(key);
|
|
42612
|
+
seen.add(key);
|
|
42613
|
+
}
|
|
42614
|
+
const pairs = [];
|
|
42615
|
+
for (const key of duplicateKeys) {
|
|
42616
|
+
const parts = key.split("\0");
|
|
42617
|
+
const label = parts[0] ?? "";
|
|
42618
|
+
const env = parts[1] === "prod" ? "prod" : "dev";
|
|
42619
|
+
pairs.push({ label, env });
|
|
42620
|
+
}
|
|
42621
|
+
return pairs;
|
|
42622
|
+
}
|
|
42358
42623
|
function nextWikiPageId(pages) {
|
|
42359
42624
|
let max = 0;
|
|
42360
42625
|
for (const page of pages) {
|
|
@@ -42365,7 +42630,7 @@ function nextWikiPageId(pages) {
|
|
|
42365
42630
|
}
|
|
42366
42631
|
return `WK-${max + 1}`;
|
|
42367
42632
|
}
|
|
42368
|
-
function buildDraftSpecPrompt(task, constitution) {
|
|
42633
|
+
function buildDraftSpecPrompt(task, constitution, deployTarget = null) {
|
|
42369
42634
|
const budget = resolveSpecBudget(task.impact);
|
|
42370
42635
|
const lines2 = [
|
|
42371
42636
|
`Write a complete implementation spec for the task below.`,
|
|
@@ -42402,6 +42667,7 @@ function buildDraftSpecPrompt(task, constitution) {
|
|
|
42402
42667
|
`The \`## TL;DR\` section MUST be at most ${DRAFT_SPEC_TLDR_MAX_CHARS} characters.`,
|
|
42403
42668
|
`The spec MUST contain at most ${budget.maxRequirements} acceptance-criteria items and no more than ${budget.maxChars} characters total.`
|
|
42404
42669
|
);
|
|
42670
|
+
if (deployTarget) lines2.push(buildDeployTargetPromptSection(deployTarget));
|
|
42405
42671
|
return lines2.join("\n");
|
|
42406
42672
|
}
|
|
42407
42673
|
function skipped(reason, extra = {}) {
|
|
@@ -42450,6 +42716,22 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42450
42716
|
}
|
|
42451
42717
|
if (getActiveBlockedByIds(task.blockedBy, blockers).length > 0) return skipped("blocked_by_active");
|
|
42452
42718
|
}
|
|
42719
|
+
const autoDeploy = task.autoDeploy ?? "off";
|
|
42720
|
+
let topology = null;
|
|
42721
|
+
let deploySkippedReason = null;
|
|
42722
|
+
let deploySkippedDetail = null;
|
|
42723
|
+
if (autoDeploy !== "off") {
|
|
42724
|
+
try {
|
|
42725
|
+
topology = await client.getDeployTopology(projectSlug);
|
|
42726
|
+
} catch {
|
|
42727
|
+
deploySkippedReason = "topology_unavailable";
|
|
42728
|
+
topology = null;
|
|
42729
|
+
}
|
|
42730
|
+
}
|
|
42731
|
+
if (topology && (topology.fingerprint === null || topology.entries.length === 0 && topology.snapshot === null)) {
|
|
42732
|
+
deploySkippedReason = topology.fingerprint === null ? "no_bound_deploy_target" : "topology_snapshot_unavailable";
|
|
42733
|
+
topology = null;
|
|
42734
|
+
}
|
|
42453
42735
|
let persona;
|
|
42454
42736
|
try {
|
|
42455
42737
|
persona = loadPersonaFn(DRAFT_SPEC_PERSONA);
|
|
@@ -42478,7 +42760,8 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42478
42760
|
} catch {
|
|
42479
42761
|
constitution = null;
|
|
42480
42762
|
}
|
|
42481
|
-
const
|
|
42763
|
+
const deployPromptContext = autoDeploy !== "off" && topology ? topology.entries.length > 0 ? { kind: "closed_menu", entries: topology.entries } : { kind: "derive", snapshot: topology.snapshot ?? [] } : null;
|
|
42764
|
+
const prompt = buildDraftSpecPrompt(task, constitution, deployPromptContext);
|
|
42482
42765
|
const exec = await runExec({
|
|
42483
42766
|
prompt,
|
|
42484
42767
|
developerInstructions: persona,
|
|
@@ -42493,7 +42776,8 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42493
42776
|
if (exec.text.trim().length === 0) {
|
|
42494
42777
|
return { kind: "retry", failureReason: "zero_output", error: "spec authoring produced empty output" };
|
|
42495
42778
|
}
|
|
42496
|
-
const { markdown,
|
|
42779
|
+
const { markdown: specMarkdown, deployTarget: parsedDeployTarget } = extractDeployTarget(exec.text);
|
|
42780
|
+
const { markdown, tldrChars } = enforceSpecOutputContract(specMarkdown, taskId, task.title);
|
|
42497
42781
|
let pageId;
|
|
42498
42782
|
if (adoptedId) {
|
|
42499
42783
|
try {
|
|
@@ -42553,6 +42837,45 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42553
42837
|
}
|
|
42554
42838
|
pageId = created;
|
|
42555
42839
|
}
|
|
42840
|
+
let deployTargets;
|
|
42841
|
+
if (topology && parsedDeployTarget) {
|
|
42842
|
+
const fingerprint = topology.fingerprint;
|
|
42843
|
+
if (fingerprint === null) {
|
|
42844
|
+
deploySkippedReason = deploySkippedReason ?? "no_bound_deploy_target";
|
|
42845
|
+
} else if (topology.entries.length > 0) {
|
|
42846
|
+
const resolved = resolveDeployTargetRows(topology.entries, parsedDeployTarget, autoDeploy, fingerprint);
|
|
42847
|
+
deployTargets = resolved.deployTargets;
|
|
42848
|
+
if (resolved.skippedReason) {
|
|
42849
|
+
deploySkippedReason = resolved.skippedReason;
|
|
42850
|
+
if (resolved.skippedReason === "incomplete_label") {
|
|
42851
|
+
deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
|
|
42852
|
+
}
|
|
42853
|
+
}
|
|
42854
|
+
} else {
|
|
42855
|
+
const derive = parsedDeployTarget.derive ?? [];
|
|
42856
|
+
const duplicateLabelEnvPairs = findDuplicateLabelEnvPairs(derive);
|
|
42857
|
+
if (derive.length === 0) {
|
|
42858
|
+
deploySkippedReason = "no_derivation";
|
|
42859
|
+
} else if (duplicateLabelEnvPairs.length > 0) {
|
|
42860
|
+
deploySkippedReason = "ambiguous_derivation";
|
|
42861
|
+
deploySkippedDetail = `duplicate label/env pair(s): ${duplicateLabelEnvPairs.map((pair) => `${pair.label}/${pair.env}`).join(", ")}`;
|
|
42862
|
+
} else {
|
|
42863
|
+
try {
|
|
42864
|
+
const written = await client.writeDeployTopology(projectSlug, { fingerprint, entries: derive });
|
|
42865
|
+
const resolved = resolveDeployTargetRows(written.entries, parsedDeployTarget, autoDeploy, fingerprint);
|
|
42866
|
+
deployTargets = resolved.deployTargets;
|
|
42867
|
+
if (resolved.skippedReason) {
|
|
42868
|
+
deploySkippedReason = resolved.skippedReason;
|
|
42869
|
+
if (resolved.skippedReason === "incomplete_label") {
|
|
42870
|
+
deploySkippedDetail = `label "${parsedDeployTarget.label}" has no entry for: ${(resolved.missingEnvs ?? []).join(", ")}`;
|
|
42871
|
+
}
|
|
42872
|
+
}
|
|
42873
|
+
} catch {
|
|
42874
|
+
deploySkippedReason = "topology_write_failed";
|
|
42875
|
+
}
|
|
42876
|
+
}
|
|
42877
|
+
}
|
|
42878
|
+
}
|
|
42556
42879
|
let fresh;
|
|
42557
42880
|
try {
|
|
42558
42881
|
fresh = await client.getTask(projectSlug, taskId);
|
|
@@ -42569,8 +42892,12 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42569
42892
|
if (fresh.specWikiPageId) {
|
|
42570
42893
|
return skipped("lost_link_race", { pageId, linkedPageId: fresh.specWikiPageId });
|
|
42571
42894
|
}
|
|
42895
|
+
const linkPatch = { specWikiPageId: pageId };
|
|
42896
|
+
if (deployTargets !== void 0 && deployTargets.length > 0) {
|
|
42897
|
+
linkPatch.deployTargets = deployTargets;
|
|
42898
|
+
}
|
|
42572
42899
|
try {
|
|
42573
|
-
await client.updateTask(projectSlug, taskId,
|
|
42900
|
+
await client.updateTask(projectSlug, taskId, linkPatch);
|
|
42574
42901
|
} catch (error51) {
|
|
42575
42902
|
return {
|
|
42576
42903
|
kind: "retry",
|
|
@@ -42578,18 +42905,20 @@ async function runDraftSpec(ctx, input, deps) {
|
|
|
42578
42905
|
error: `failed to link spec page ${pageId} to task ${taskId}: ${errMessage2(error51)}`
|
|
42579
42906
|
};
|
|
42580
42907
|
}
|
|
42908
|
+
const drafted = {
|
|
42909
|
+
outcome: "DRAFTED",
|
|
42910
|
+
pageId,
|
|
42911
|
+
adopted: adoptedId !== null,
|
|
42912
|
+
constitution: constitution !== null ? "included" : "missing",
|
|
42913
|
+
tldrChars,
|
|
42914
|
+
relatedContext: "skipped_no_client_method"
|
|
42915
|
+
};
|
|
42916
|
+
if (deploySkippedReason !== null) drafted.deployTargetSkippedReason = deploySkippedReason;
|
|
42917
|
+
if (deploySkippedDetail !== null) drafted.deployTargetSkippedDetail = deploySkippedDetail;
|
|
42918
|
+
drafted.taskTitle = task.title;
|
|
42581
42919
|
return {
|
|
42582
42920
|
kind: "done",
|
|
42583
|
-
resultJson: JSON.stringify(
|
|
42584
|
-
outcome: "DRAFTED",
|
|
42585
|
-
pageId,
|
|
42586
|
-
adopted: adoptedId !== null,
|
|
42587
|
-
constitution: constitution !== null ? "included" : "missing",
|
|
42588
|
-
tldrChars,
|
|
42589
|
-
relatedContext: "skipped_no_client_method",
|
|
42590
|
-
// Carried so onComplete can name the task in the operator notification without a re-read.
|
|
42591
|
-
taskTitle: task.title
|
|
42592
|
-
}),
|
|
42921
|
+
resultJson: JSON.stringify(drafted),
|
|
42593
42922
|
finalReviewedHash: null
|
|
42594
42923
|
};
|
|
42595
42924
|
}
|
|
@@ -42607,6 +42936,14 @@ function onComplete(outcome, input) {
|
|
|
42607
42936
|
body: `Spec draft for ${input.taskId} skipped (${parsed.reason ?? "unknown"}); nothing changed.`
|
|
42608
42937
|
};
|
|
42609
42938
|
}
|
|
42939
|
+
if (parsed.deployTargetSkippedReason === "ambiguous_derivation" || parsed.deployTargetSkippedReason === "incomplete_label") {
|
|
42940
|
+
return {
|
|
42941
|
+
kind: "handoff",
|
|
42942
|
+
taskId: input.taskId,
|
|
42943
|
+
id: `II-deploy-target-${parsed.deployTargetSkippedReason}-${input.taskId}`,
|
|
42944
|
+
body: `Spec for ${input.taskId} was drafted (page ${parsed.pageId ?? "?"} in Specs/), but its deploy target needs a human: ${parsed.deployTargetSkippedDetail ?? parsed.deployTargetSkippedReason}. The spec itself proceeds to the normal consensus review; the deploy target stays unresolved until this is fixed.`
|
|
42945
|
+
};
|
|
42946
|
+
}
|
|
42610
42947
|
const taskRef = parsed.taskTitle ? `${input.taskId} \u2014 ${parsed.taskTitle}` : input.taskId;
|
|
42611
42948
|
return {
|
|
42612
42949
|
kind: "info",
|
|
@@ -42980,34 +43317,42 @@ async function tryProdAutoRevert(deps, projectSlug, run3, task) {
|
|
|
42980
43317
|
return null;
|
|
42981
43318
|
}
|
|
42982
43319
|
}
|
|
42983
|
-
function
|
|
42984
|
-
return workflows.map((workflow) => `${workflow.id}:${workflow.path}`).sort().join(",");
|
|
42985
|
-
}
|
|
42986
|
-
async function resolveWorkflowId(deps, projectSlug, run3, env) {
|
|
43320
|
+
async function resolveWorkflowId(deps, projectSlug, run3, task, env) {
|
|
42987
43321
|
const pinned = env === "dev" ? run3.devWorkflowId : run3.prodWorkflowId;
|
|
42988
|
-
if (pinned)
|
|
42989
|
-
|
|
42990
|
-
if (!row || row.state !== "resolved" || row.workflowId === null) {
|
|
42991
|
-
return { workflowId: null, source: null, resolvedFromStore: false };
|
|
42992
|
-
}
|
|
42993
|
-
if (row.source !== "operator") {
|
|
42994
|
-
const liveWorkflows = await deps.client.listGithubWorkflows(projectSlug);
|
|
42995
|
-
const activeWorkflows = liveWorkflows.filter((workflow) => workflow.state === "active");
|
|
42996
|
-
const isLive = activeWorkflows.some((workflow) => String(workflow.id) === String(row.workflowId));
|
|
42997
|
-
if (!isLive) return { workflowId: null, source: null, resolvedFromStore: false };
|
|
42998
|
-
const liveFingerprint = computeActiveSetFingerprint(activeWorkflows);
|
|
42999
|
-
if (!row.reverifyRequested && row.activeSetFingerprint !== null && liveFingerprint !== row.activeSetFingerprint) {
|
|
43000
|
-
await deps.client.upsertProjectDeployWorkflow(projectSlug, env, {
|
|
43001
|
-
workflowId: row.workflowId,
|
|
43002
|
-
workflowPath: row.workflowPath,
|
|
43003
|
-
reason: row.reason,
|
|
43004
|
-
state: row.state,
|
|
43005
|
-
activeSetFingerprint: row.activeSetFingerprint,
|
|
43006
|
-
reverifyRequested: true
|
|
43007
|
-
});
|
|
43008
|
-
}
|
|
43322
|
+
if (pinned) {
|
|
43323
|
+
return { workflowId: pinned, workflowPath: null, blockReason: null, pinned: true, reverifyRequested: false };
|
|
43009
43324
|
}
|
|
43010
|
-
|
|
43325
|
+
const target = await deps.getTaskDeployTarget(projectSlug, task.id, env);
|
|
43326
|
+
if (!target) {
|
|
43327
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_unset", pinned: false, reverifyRequested: false };
|
|
43328
|
+
}
|
|
43329
|
+
const snapshot = await deps.getRepoSnapshot(projectSlug);
|
|
43330
|
+
const activeWorkflows = (snapshot?.lastSnapshot ?? []).filter((w) => w.state === "active");
|
|
43331
|
+
const liveMatch = activeWorkflows.find((w) => w.id === target.workflowId);
|
|
43332
|
+
if (!liveMatch || liveMatch.path !== target.workflowPath) {
|
|
43333
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_inactive", pinned: false, reverifyRequested: false };
|
|
43334
|
+
}
|
|
43335
|
+
if (liveMatch.sha !== target.workflowSha) {
|
|
43336
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_modified", pinned: false, reverifyRequested: false };
|
|
43337
|
+
}
|
|
43338
|
+
const currentFingerprint = snapshot?.lastFingerprint ?? null;
|
|
43339
|
+
if (currentFingerprint !== null && currentFingerprint === target.topologyFingerprint) {
|
|
43340
|
+
return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: false };
|
|
43341
|
+
}
|
|
43342
|
+
if (currentFingerprint === null) {
|
|
43343
|
+
deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
|
|
43344
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
|
|
43345
|
+
}
|
|
43346
|
+
const entry = await deps.lookupTopologyEntry(projectSlug, currentFingerprint, target.targetLabel, env);
|
|
43347
|
+
if (!entry) {
|
|
43348
|
+
deps.recordDeployTargetTelemetry("deploy_target_cache_missing");
|
|
43349
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_cache_missing", pinned: false, reverifyRequested: false };
|
|
43350
|
+
}
|
|
43351
|
+
if (entry.workflowId !== target.workflowId) {
|
|
43352
|
+
deps.recordDeployTargetTelemetry("deploy_target_reassigned");
|
|
43353
|
+
return { workflowId: null, workflowPath: null, blockReason: "deploy_target_reassigned", pinned: false, reverifyRequested: false };
|
|
43354
|
+
}
|
|
43355
|
+
return { workflowId: target.workflowId, workflowPath: target.workflowPath, blockReason: null, pinned: false, reverifyRequested: true };
|
|
43011
43356
|
}
|
|
43012
43357
|
async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
43013
43358
|
const artifacts = await deps.client.listExecutionArtifacts(projectSlug, run3.id);
|
|
@@ -43029,7 +43374,10 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
|
43029
43374
|
return { kind: "done" };
|
|
43030
43375
|
}
|
|
43031
43376
|
const env = activeEnv;
|
|
43032
|
-
const resolution = await resolveWorkflowId(deps, projectSlug, run3, env);
|
|
43377
|
+
const resolution = await resolveWorkflowId(deps, projectSlug, run3, task, env);
|
|
43378
|
+
if (resolution.reverifyRequested) {
|
|
43379
|
+
await deps.setTaskReverifyRequested(projectSlug, task.id);
|
|
43380
|
+
}
|
|
43033
43381
|
const workflowId = resolution.workflowId;
|
|
43034
43382
|
const attempt = latestByEnv(artifacts, "deploy_attempt", env, { realAttemptsOnly: true });
|
|
43035
43383
|
if (!attempt) {
|
|
@@ -43056,9 +43404,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
|
43056
43404
|
await deps.handoff(projectSlug, task, run3, "verification_steps_missing");
|
|
43057
43405
|
return { kind: "handoff", reason: "verification_steps_missing" };
|
|
43058
43406
|
}
|
|
43407
|
+
if (resolution.blockReason) {
|
|
43408
|
+
await deps.handoff(projectSlug, task, run3, `${resolution.blockReason}:${env}`);
|
|
43409
|
+
return { kind: "handoff", reason: `${resolution.blockReason}:${env}` };
|
|
43410
|
+
}
|
|
43059
43411
|
if (!workflowId) {
|
|
43060
|
-
await deps.handoff(projectSlug, task, run3, `
|
|
43061
|
-
return { kind: "handoff", reason: `
|
|
43412
|
+
await deps.handoff(projectSlug, task, run3, `deploy_target_unset:${env}`);
|
|
43413
|
+
return { kind: "handoff", reason: `deploy_target_unset:${env}` };
|
|
43062
43414
|
}
|
|
43063
43415
|
if (!run3.branch) {
|
|
43064
43416
|
await deps.handoff(projectSlug, task, run3, "run_branch_missing");
|
|
@@ -43076,19 +43428,13 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
|
|
|
43076
43428
|
try {
|
|
43077
43429
|
await deps.client.dispatchTaskDeploy(projectSlug, task.id, { env, ref: run3.branch, workflowId: String(workflowId) });
|
|
43078
43430
|
} catch (error51) {
|
|
43079
|
-
if (resolution.
|
|
43080
|
-
|
|
43081
|
-
projectSlug,
|
|
43082
|
-
task,
|
|
43083
|
-
run3,
|
|
43084
|
-
`deploy_workflow_operator_choice_stale:${env}:${String(error51).slice(0, 120)}`
|
|
43085
|
-
);
|
|
43086
|
-
return { kind: "handoff", reason: `deploy_workflow_operator_choice_stale:${env}` };
|
|
43431
|
+
if (!resolution.pinned && !resolution.reverifyRequested) {
|
|
43432
|
+
deps.recordDeployTargetTelemetry("deploy_dispatch_failed_fingerprint_unchanged");
|
|
43087
43433
|
}
|
|
43088
43434
|
await deps.handoff(projectSlug, task, run3, `workflow_dispatch_failed:${env}:${String(error51).slice(0, 120)}`);
|
|
43089
43435
|
return { kind: "handoff", reason: `workflow_dispatch_failed:${env}` };
|
|
43090
43436
|
}
|
|
43091
|
-
if (resolution.
|
|
43437
|
+
if (!resolution.pinned) {
|
|
43092
43438
|
await deps.client.updateExecutionRun(projectSlug, run3.id, {
|
|
43093
43439
|
[env === "dev" ? "devWorkflowId" : "prodWorkflowId"]: workflowId
|
|
43094
43440
|
});
|
|
@@ -43731,9 +44077,14 @@ var BASE_INSTRUCTIONS_PREFIX = [
|
|
|
43731
44077
|
" if you are blocked and a human must decide before the work can continue, leave it at `handoff_needed`.",
|
|
43732
44078
|
"",
|
|
43733
44079
|
"File artifacts:",
|
|
43734
|
-
"- Decide BEFORE writing your result
|
|
43735
|
-
"
|
|
43736
|
-
"
|
|
44080
|
+
"- Decide BEFORE writing your result, by PROVENANCE: output assembled from data you RETRIEVED",
|
|
44081
|
+
" this run (API, browser, analytics, database, a search over project records) that comes out as",
|
|
44082
|
+
" a multi-record or multi-dimension result is a FILE, not inline text.",
|
|
44083
|
+
"- Your own synthesis or comparison stays inline however structured it looks, and so does a",
|
|
44084
|
+
" retrieval yielding one value. Both conditions must hold before it becomes a file. When it",
|
|
44085
|
+
" qualifies, produce the file; never merely offer one.",
|
|
44086
|
+
"- Mixed output counts as retrieved: if any material part came from a retrieval, treat the whole",
|
|
44087
|
+
" result as retrieved rather than splitting it between inline text and a file.",
|
|
43737
44088
|
"- When your output is a file (markdown report, HTML, screenshot), upload it with",
|
|
43738
44089
|
" `upload_artifact` and embed the returned markdown link \u2014 never inline large blobs",
|
|
43739
44090
|
" into chat, spec, or note text.",
|
|
@@ -45180,7 +45531,35 @@ async function runBackstopSweep(deps, drive = (slug, id) => driveRun(deps, slug,
|
|
|
45180
45531
|
await deployGateHandoff(deps, projectSlug, t, r.id, reason, r.leaseId);
|
|
45181
45532
|
},
|
|
45182
45533
|
budgetSeconds: deps.deployGate.budgetSeconds ?? deployPollBudgetSeconds(),
|
|
45183
|
-
now: () => /* @__PURE__ */ new Date()
|
|
45534
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
45535
|
+
getTaskDeployTarget: async (projectSlug, taskId, env) => deps.client.getTaskDeployTarget(projectSlug, taskId, env),
|
|
45536
|
+
getRepoSnapshot: async (projectSlug) => {
|
|
45537
|
+
try {
|
|
45538
|
+
const topology = await deps.client.getDeployTopology(projectSlug);
|
|
45539
|
+
return { lastFingerprint: topology.fingerprint, lastSnapshot: topology.snapshot };
|
|
45540
|
+
} catch {
|
|
45541
|
+
return null;
|
|
45542
|
+
}
|
|
45543
|
+
},
|
|
45544
|
+
lookupTopologyEntry: async (projectSlug, fingerprint, label, env) => {
|
|
45545
|
+
try {
|
|
45546
|
+
const topology = await deps.client.getDeployTopology(projectSlug);
|
|
45547
|
+
if (topology.fingerprint !== fingerprint) return null;
|
|
45548
|
+
const entry = topology.entries.find((e) => e.label === label && e.env === env);
|
|
45549
|
+
return entry ? { workflowId: entry.workflowId, workflowPath: entry.workflowPath, workflowSha: entry.workflowSha } : null;
|
|
45550
|
+
} catch {
|
|
45551
|
+
return null;
|
|
45552
|
+
}
|
|
45553
|
+
},
|
|
45554
|
+
setTaskReverifyRequested: async (projectSlug, taskId) => {
|
|
45555
|
+
await deps.client.updateTask(projectSlug, taskId, { reverifyRequested: true }).catch(() => {
|
|
45556
|
+
});
|
|
45557
|
+
},
|
|
45558
|
+
recordDeployTargetTelemetry: (counter) => {
|
|
45559
|
+
console.log(
|
|
45560
|
+
JSON.stringify({ event: "deploy_target_telemetry", counter, projectSlug: slug, taskId: task.id })
|
|
45561
|
+
);
|
|
45562
|
+
}
|
|
45184
45563
|
};
|
|
45185
45564
|
await runDeployGateTick(gateDeps, slug, run3, task);
|
|
45186
45565
|
} catch {
|
|
@@ -50366,12 +50745,13 @@ var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
|
|
|
50366
50745
|
"http://localhost:*",
|
|
50367
50746
|
"http://127.0.0.1:*",
|
|
50368
50747
|
"http://[::1]:*",
|
|
50369
|
-
// Staging
|
|
50370
|
-
//
|
|
50371
|
-
//
|
|
50372
|
-
//
|
|
50373
|
-
|
|
50374
|
-
"https://
|
|
50748
|
+
// Staging decoupled onto the stable custom domain staging.mrrlin.com on 2026-08-14 (Cloudflare DNS
|
|
50749
|
+
// in front of the mrrlin3 Vercel project). The Vercel-owned web alias (mrrlin-web-mrrlin3.vercel.app)
|
|
50750
|
+
// and the old app.mrrlin.dev origin are dropped from the defaults: the browser now reaches the
|
|
50751
|
+
// bridge from the owned domain, which is stable across future Vercel project/team moves.
|
|
50752
|
+
// See docs/reference/deployments.md.
|
|
50753
|
+
"https://staging.mrrlin.com",
|
|
50754
|
+
// Production web (unchanged).
|
|
50375
50755
|
"https://mrrlin-web.vercel.app"
|
|
50376
50756
|
];
|
|
50377
50757
|
function normalizeReasoningEffort(value) {
|
|
@@ -50669,9 +51049,17 @@ function buildThreadStartParams(msg, config2, desiredModel) {
|
|
|
50669
51049
|
" is not measurable, re-set `verificationDueAt` to a new date with a reason.",
|
|
50670
51050
|
"",
|
|
50671
51051
|
"File artifacts contract \u2014 decide the delivery medium BEFORE you compose the reply:",
|
|
50672
|
-
"-
|
|
50673
|
-
"
|
|
50674
|
-
"
|
|
51052
|
+
"- Decide by PROVENANCE, not by predicting what the operator will do later. If this answer is",
|
|
51053
|
+
" assembled from data you RETRIEVED this turn (an HTTP/API call, the browser, analytics, a",
|
|
51054
|
+
" database, a search over project records) AND it comes out as a multi-record or",
|
|
51055
|
+
" multi-dimension result rather than one fact or a few values, it belongs in a FILE.",
|
|
51056
|
+
"- Your own synthesis, comparison, or judgement belongs in chat, however structured it looks.",
|
|
51057
|
+
" A retrieval that yields one number or one line also belongs in chat. Both conditions must",
|
|
51058
|
+
" hold before it becomes a file.",
|
|
51059
|
+
"- Mixed answers count as retrieved: if any material part of the result came from a retrieval,",
|
|
51060
|
+
" treat the whole answer as retrieved rather than splitting it between chat and a file.",
|
|
51061
|
+
'- When it qualifies, PRODUCE the file. Offering one ("I can give you this as CSV/JSON") is the',
|
|
51062
|
+
" failure, not the courtesy.",
|
|
50675
51063
|
"- When it is a file: write it (`.md` for linear prose, `.html` when structure or layout",
|
|
50676
51064
|
" carries meaning), upload with `upload_artifact` (class=temp for one-off downloads,",
|
|
50677
51065
|
" class=durable when a spec/evidence page references it; pass taskId when known), and paste",
|
|
@@ -51843,7 +52231,18 @@ function createBridgeMessageHandler(deps) {
|
|
|
51843
52231
|
});
|
|
51844
52232
|
const mapper = thread.mapper;
|
|
51845
52233
|
mapper.reset();
|
|
51846
|
-
const
|
|
52234
|
+
const contextFocusTaskId = msg.context?.focusTaskId?.trim();
|
|
52235
|
+
const contextProjectSlug = msg.context?.projectSlug?.trim();
|
|
52236
|
+
let answeredQuestions;
|
|
52237
|
+
if (deps.listAnsweredQuestions && contextFocusTaskId && contextProjectSlug) {
|
|
52238
|
+
try {
|
|
52239
|
+
const acknowledged = await deps.listAnsweredQuestions(contextProjectSlug, contextFocusTaskId);
|
|
52240
|
+
const withAnswers = acknowledged.filter((item) => Boolean(item.answer)).map((item) => ({ answer: item.answer, question: item.body }));
|
|
52241
|
+
answeredQuestions = withAnswers.length ? withAnswers : void 0;
|
|
52242
|
+
} catch {
|
|
52243
|
+
}
|
|
52244
|
+
}
|
|
52245
|
+
const contextPrefix = renderContextPrefix({ ...msg.context, answeredQuestions, directorSessionId });
|
|
51847
52246
|
const sendInTurn = (msg2) => {
|
|
51848
52247
|
const buf = thread.replay;
|
|
51849
52248
|
if (!buf) {
|
|
@@ -52529,7 +52928,7 @@ async function startDirectorBridge() {
|
|
|
52529
52928
|
if (!body) return;
|
|
52530
52929
|
const id = `II-MCPUPD-${(0, import_node_crypto8.randomUUID)()}`;
|
|
52531
52930
|
try {
|
|
52532
|
-
const row = await client.createInboxItem(projectSlug, { id, kind: "
|
|
52931
|
+
const row = await client.createInboxItem(projectSlug, { id, kind: "info", body });
|
|
52533
52932
|
process.stderr.write(
|
|
52534
52933
|
`[mrrlin-mcp director-bridge] info bridge.self_update.inbox_filed ${JSON.stringify({
|
|
52535
52934
|
projectSlug,
|
|
@@ -52549,6 +52948,7 @@ async function startDirectorBridge() {
|
|
|
52549
52948
|
);
|
|
52550
52949
|
}
|
|
52551
52950
|
},
|
|
52951
|
+
listAnsweredQuestions: (projectSlug, taskId) => client.listInboxItems(projectSlug, { status: "acknowledged", kind: "question", taskId }),
|
|
52552
52952
|
// External-agents control plane: the real spawn runner, with the EXTERNAL_AGENTS_CLI operator
|
|
52553
52953
|
// escape hatch bound here (the handlers stay spawn-free and this is the only production seam).
|
|
52554
52954
|
runExternalAgentsCli: (args, opts) => runExternalAgentsCli(args, { ...opts, envCliPath: process.env["EXTERNAL_AGENTS_CLI"] })
|
|
@@ -52963,11 +53363,13 @@ function renderContextPrefix(context) {
|
|
|
52963
53363
|
const urlPath = context?.urlPath?.trim() ? context.urlPath.trim() : void 0;
|
|
52964
53364
|
const focusTaskId = context?.focusTaskId?.trim() ? context.focusTaskId.trim() : void 0;
|
|
52965
53365
|
const directorSessionId = context?.directorSessionId?.trim() ? context.directorSessionId.trim() : void 0;
|
|
53366
|
+
const answeredQuestions = context?.answeredQuestions?.length ? context.answeredQuestions : void 0;
|
|
52966
53367
|
const payload = {};
|
|
52967
53368
|
if (projectSlug) payload.projectSlug = projectSlug;
|
|
52968
53369
|
if (urlPath) payload.urlPath = urlPath;
|
|
52969
53370
|
if (focusTaskId) payload.focusTaskId = focusTaskId;
|
|
52970
53371
|
if (directorSessionId) payload.directorSessionId = directorSessionId;
|
|
53372
|
+
if (answeredQuestions) payload.answeredQuestions = answeredQuestions;
|
|
52971
53373
|
const keys = Object.keys(payload);
|
|
52972
53374
|
if (keys.length === 0) return "";
|
|
52973
53375
|
return `Context (JSON): ${JSON.stringify(payload)}`;
|
|
@@ -57546,6 +57948,8 @@ var mcpToolNames = {
|
|
|
57546
57948
|
resetDirectorThread: "reset_director_thread",
|
|
57547
57949
|
archiveTask: "archive_task",
|
|
57548
57950
|
unarchiveTask: "unarchive_task",
|
|
57951
|
+
takeOverTask: "take_over_task",
|
|
57952
|
+
closeTaskAsWontDo: "close_task_as_wont_do",
|
|
57549
57953
|
updateWikiPage: "update_wiki_page",
|
|
57550
57954
|
markSpecReady: "mark_spec_ready",
|
|
57551
57955
|
runSpecConsensus: "run_spec_consensus",
|
|
@@ -58052,6 +58456,8 @@ var updateTaskInputSchema = taskScopedSchema.merge(mrrlinTaskUpdateSchema);
|
|
|
58052
58456
|
var resetDirectorThreadInputSchema = taskScopedSchema;
|
|
58053
58457
|
var archiveTaskInputSchema = taskScopedSchema;
|
|
58054
58458
|
var unarchiveTaskInputSchema = taskScopedSchema;
|
|
58459
|
+
var takeOverTaskInputSchema = taskScopedSchema.merge(mrrlinTakeOverTaskRequestSchema);
|
|
58460
|
+
var closeTaskAsWontDoInputSchema = taskScopedSchema.merge(mrrlinCloseTaskAsWontDoRequestSchema);
|
|
58055
58461
|
var listPlansInputSchema = projectScopedSchema;
|
|
58056
58462
|
var createPlanInputSchema = projectScopedSchema.merge(mrrlinPlanCreateSchema);
|
|
58057
58463
|
var updatePlanInputSchema = planScopedSchema.merge(mrrlinPlanUpdateSchema);
|
|
@@ -58209,6 +58615,8 @@ var mcpToolInputSchemas = {
|
|
|
58209
58615
|
[mcpToolNames.resetDirectorThread]: resetDirectorThreadInputSchema,
|
|
58210
58616
|
[mcpToolNames.archiveTask]: archiveTaskInputSchema,
|
|
58211
58617
|
[mcpToolNames.unarchiveTask]: unarchiveTaskInputSchema,
|
|
58618
|
+
[mcpToolNames.takeOverTask]: takeOverTaskInputSchema,
|
|
58619
|
+
[mcpToolNames.closeTaskAsWontDo]: closeTaskAsWontDoInputSchema,
|
|
58212
58620
|
[mcpToolNames.updateWikiPage]: updateWikiPageInputSchema,
|
|
58213
58621
|
[mcpToolNames.markSpecReady]: markSpecReadyInputSchema,
|
|
58214
58622
|
[mcpToolNames.runSpecConsensus]: runSpecConsensusInputSchema,
|
|
@@ -58278,6 +58686,8 @@ var mcpToolOperationIds = {
|
|
|
58278
58686
|
[mcpToolNames.resetDirectorThread]: openApiOperationIds.resetDirectorThread,
|
|
58279
58687
|
[mcpToolNames.archiveTask]: openApiOperationIds.archiveTask,
|
|
58280
58688
|
[mcpToolNames.unarchiveTask]: openApiOperationIds.unarchiveTask,
|
|
58689
|
+
[mcpToolNames.takeOverTask]: openApiOperationIds.takeOverTask,
|
|
58690
|
+
[mcpToolNames.closeTaskAsWontDo]: openApiOperationIds.closeTaskAsWontDo,
|
|
58281
58691
|
[mcpToolNames.updateWikiPage]: openApiOperationIds.updateWikiPage,
|
|
58282
58692
|
[mcpToolNames.markSpecReady]: openApiOperationIds.markSpecReady,
|
|
58283
58693
|
[mcpToolNames.listInboxItems]: openApiOperationIds.listInboxItems,
|
|
@@ -58329,6 +58739,8 @@ var mcpToolDescriptions = {
|
|
|
58329
58739
|
[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.",
|
|
58330
58740
|
[mcpToolNames.archiveTask]: "Archive a task: hides it from default reads, removes it from plan progress and scheduler consideration, cancels its non-terminal execution runs, and closes its open inbox items. Reversible \u2014 the project owner can restore it with unarchive_task. Records an 'archived' task event.",
|
|
58331
58741
|
[mcpToolNames.unarchiveTask]: "Restore an archived task to its original status and visibility. Owner-only \u2014 refused with NOT_PROJECT_OWNER for any other caller. Does not resurrect the execution runs or inbox items that were cancelled/closed when the task was archived. Records a 'restored' task event.",
|
|
58742
|
+
[mcpToolNames.takeOverTask]: "Take ownership of a task by assigning it to the provided assignee through the dedicated take-over route.",
|
|
58743
|
+
[mcpToolNames.closeTaskAsWontDo]: "Close a task as won't-do with a required human-readable reason recorded on the task.",
|
|
58332
58744
|
[mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
|
|
58333
58745
|
[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.",
|
|
58334
58746
|
[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 low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus, using the same flow as code tasks. Report the spec\'s review outcome to the operator.',
|
|
@@ -58776,6 +59188,20 @@ function createMrrlinTools(options) {
|
|
|
58776
59188
|
"Unable to unarchive task.",
|
|
58777
59189
|
async (c, { projectSlug, taskId }) => await c.unarchiveTask(projectSlug, taskId)
|
|
58778
59190
|
),
|
|
59191
|
+
[mcpToolNames.takeOverTask]: makeTool(
|
|
59192
|
+
client,
|
|
59193
|
+
mcpToolNames.takeOverTask,
|
|
59194
|
+
"TASK_TAKE_OVER_FAILED",
|
|
59195
|
+
"Unable to take over task.",
|
|
59196
|
+
async (c, { projectSlug, taskId, assignee }) => await c.takeOverTask(projectSlug, taskId, assignee)
|
|
59197
|
+
),
|
|
59198
|
+
[mcpToolNames.closeTaskAsWontDo]: makeTool(
|
|
59199
|
+
client,
|
|
59200
|
+
mcpToolNames.closeTaskAsWontDo,
|
|
59201
|
+
"TASK_CLOSE_AS_WONT_DO_FAILED",
|
|
59202
|
+
"Unable to close task as won't-do.",
|
|
59203
|
+
async (c, { projectSlug, taskId, reason }) => await c.closeTaskAsWontDo(projectSlug, taskId, reason)
|
|
59204
|
+
),
|
|
58779
59205
|
[mcpToolNames.updateWikiPage]: makeTool(
|
|
58780
59206
|
client,
|
|
58781
59207
|
mcpToolNames.updateWikiPage,
|
|
@@ -59398,13 +59824,15 @@ function installService(deps) {
|
|
|
59398
59824
|
}
|
|
59399
59825
|
const resolvedBins = bins.filter((b) => b !== null);
|
|
59400
59826
|
const pathEnv = Array.from(new Set(resolvedBins.map((b) => import_node_path21.default.dirname(b)))).join(":");
|
|
59827
|
+
const apiBaseUrl = deps.env.MRRLIN_API_BASE_URL?.trim() || void 0;
|
|
59828
|
+
const staging = deps.env.MRRLIN_STAGING === "1" || !apiBaseUrl && bridgeUrlImpliesStaging(deps.env.MRRLIN_BRIDGE_URL);
|
|
59401
59829
|
const text = renderEcosystemConfig({
|
|
59402
59830
|
cwd: resolved.cwd,
|
|
59403
59831
|
home,
|
|
59404
59832
|
codexHome: deps.env.CODEX_HOME?.trim() || void 0,
|
|
59405
59833
|
pathEnv,
|
|
59406
|
-
staging
|
|
59407
|
-
apiBaseUrl
|
|
59834
|
+
staging,
|
|
59835
|
+
apiBaseUrl
|
|
59408
59836
|
});
|
|
59409
59837
|
const ecoPath = import_node_path21.default.join(home, ".mrrlin", "ecosystem.config.cjs");
|
|
59410
59838
|
deps.writeFile(ecoPath, text);
|
|
@@ -59527,22 +59955,22 @@ Usage:
|
|
|
59527
59955
|
mrrlin-mcp install-browser Idempotently install the dedicated, pinned
|
|
59528
59956
|
Chromium into ~/.mrrlin/browser/browsers (its own
|
|
59529
59957
|
copy, isolated from the operator's Chrome and
|
|
59530
|
-
codex-chrome). Run by
|
|
59958
|
+
codex-chrome). Run by configure bridge; safe to
|
|
59531
59959
|
re-run. Exits non-zero on download failure.
|
|
59532
59960
|
|
|
59533
59961
|
mrrlin-mcp browser-serve Launch the vendored @playwright/mcp against the
|
|
59534
59962
|
dedicated persistent profile
|
|
59535
59963
|
(~/.mrrlin/browser/profile, headed). This is the
|
|
59536
|
-
command
|
|
59964
|
+
command configure bridge wires for the
|
|
59537
59965
|
[mcp_servers.mrrlin-browser] block.
|
|
59538
59966
|
|
|
59539
59967
|
mrrlin-mcp external-agents-serve
|
|
59540
59968
|
Launch the bundled @mrrlin-dev/external-agents
|
|
59541
59969
|
MCP server (multi-LLM dispatch pool). This is the
|
|
59542
|
-
command
|
|
59970
|
+
command configure bridge wires for the
|
|
59543
59971
|
[mcp_servers.external-agents] block.
|
|
59544
59972
|
|
|
59545
|
-
mrrlin-mcp
|
|
59973
|
+
mrrlin-mcp configure bridge Idempotently register Mrrlin in
|
|
59546
59974
|
[--force] ~/.codex/config.toml (or CODEX_HOME). Adds an
|
|
59547
59975
|
[mcp_servers.mrrlin] block. Development
|
|
59548
59976
|
checkouts register the local dist/bin.cjs;
|
|
@@ -59551,12 +59979,12 @@ Usage:
|
|
|
59551
59979
|
Bakes API-target env vars into the block as an
|
|
59552
59980
|
inline env table so \`serve\` boots even when the
|
|
59553
59981
|
local 127.0.0.1:8787 is not running. Setup guide:
|
|
59554
|
-
MRRLIN_STAGING=1 mrrlin-mcp
|
|
59982
|
+
MRRLIN_STAGING=1 mrrlin-mcp configure bridge
|
|
59555
59983
|
is the canonical path (staging alias + remote-
|
|
59556
59984
|
allow gate in one flag). For a non-staging remote,
|
|
59557
59985
|
pass BOTH the URL AND the allow gate:
|
|
59558
59986
|
MRRLIN_API_BASE_URL=https://... \\
|
|
59559
|
-
MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp
|
|
59987
|
+
MRRLIN_API_ALLOW_REMOTE=1 mrrlin-mcp configure bridge
|
|
59560
59988
|
Without an allow gate, \`serve\` still refuses any
|
|
59561
59989
|
non-loopback URL (see \`mrrlin-mcp serve\` above).
|
|
59562
59990
|
URL and MRRLIN_STAGING are mutually exclusive at
|
|
@@ -59669,8 +60097,8 @@ Usage:
|
|
|
59669
60097
|
mrrlin-mcp --help Show this help.
|
|
59670
60098
|
|
|
59671
60099
|
Operator notes:
|
|
59672
|
-
-
|
|
59673
|
-
-
|
|
60100
|
+
- configure bridge is idempotent: a second run with the same target is a no-op.
|
|
60101
|
+
- configure bridge auto-detects how it was installed. Development checkouts keep
|
|
59674
60102
|
using the local dist/bin.cjs; npm installs use
|
|
59675
60103
|
\`mrrlin-mcp serve\`.
|
|
59676
60104
|
`;
|
|
@@ -59705,16 +60133,29 @@ async function main() {
|
|
|
59705
60133
|
process.exitCode = r.status ?? 1;
|
|
59706
60134
|
return;
|
|
59707
60135
|
}
|
|
59708
|
-
case "
|
|
59709
|
-
const
|
|
60136
|
+
case "configure": {
|
|
60137
|
+
const [target, ...configureRest] = rest;
|
|
60138
|
+
if (target !== "bridge") {
|
|
60139
|
+
process.stderr.write(
|
|
60140
|
+
`configure requires a target. The only supported form is:
|
|
60141
|
+
mrrlin-mcp configure bridge [--force]
|
|
60142
|
+
`
|
|
60143
|
+
);
|
|
60144
|
+
if (target !== void 0) {
|
|
60145
|
+
process.stderr.write(`Unknown configure target: ${target}
|
|
60146
|
+
`);
|
|
60147
|
+
}
|
|
60148
|
+
process.exit(2);
|
|
60149
|
+
}
|
|
60150
|
+
const force = configureRest.includes("--force");
|
|
59710
60151
|
const binPath = resolveSelfBinPath();
|
|
59711
60152
|
try {
|
|
59712
60153
|
const b = await installBrowser();
|
|
59713
|
-
process.stderr.write(`[mrrlin-mcp
|
|
60154
|
+
process.stderr.write(`[mrrlin-mcp configure bridge] install-browser ${b.action} ${b.browsersPath}
|
|
59714
60155
|
`);
|
|
59715
60156
|
} catch (error51) {
|
|
59716
60157
|
process.stderr.write(
|
|
59717
|
-
`[mrrlin-mcp
|
|
60158
|
+
`[mrrlin-mcp configure bridge] install-browser FAILED \u2014 config.toml left untouched: ${error51 instanceof Error ? error51.message : String(error51)}
|
|
59718
60159
|
`
|
|
59719
60160
|
);
|
|
59720
60161
|
process.exit(2);
|
|
@@ -59725,18 +60166,18 @@ async function main() {
|
|
|
59725
60166
|
force,
|
|
59726
60167
|
serveEnv
|
|
59727
60168
|
});
|
|
59728
|
-
process.stderr.write(`[mrrlin-mcp
|
|
60169
|
+
process.stderr.write(`[mrrlin-mcp configure bridge] ${result.action} ${result.configPath}
|
|
59729
60170
|
`);
|
|
59730
60171
|
for (const preserved of result.preservedEnvBlocks) {
|
|
59731
60172
|
process.stderr.write(`${renderEnvPreservedNotice(preserved.blockId, preserved.envKeys)}
|
|
59732
60173
|
`);
|
|
59733
60174
|
}
|
|
59734
60175
|
for (const p of result.prompts) {
|
|
59735
|
-
process.stderr.write(`[mrrlin-mcp
|
|
60176
|
+
process.stderr.write(`[mrrlin-mcp configure bridge] prompt ${p.name}: ${p.action} ${p.promptPath}
|
|
59736
60177
|
`);
|
|
59737
60178
|
if (p.action === "skipped-modified") {
|
|
59738
60179
|
process.stderr.write(
|
|
59739
|
-
`[mrrlin-mcp
|
|
60180
|
+
`[mrrlin-mcp configure bridge] prompt ${p.name}: kept your local edits; run with --force-prompts to overwrite (or delete ${p.promptPath} to take the new bundled version)
|
|
59740
60181
|
`
|
|
59741
60182
|
);
|
|
59742
60183
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrlin-dev/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mrrlin-mcp": "dist/bin.cjs"
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"@types/ws": "^8.18.1",
|
|
22
22
|
"esbuild": "^0.24.0",
|
|
23
23
|
"tsx": "^4.22.3",
|
|
24
|
+
"@mrrlin/client": "0.0.0",
|
|
24
25
|
"@mrrlin/codex-client": "0.0.0",
|
|
25
26
|
"@mrrlin/director-e2e": "0.0.0",
|
|
26
|
-
"@mrrlin/client": "0.0.0",
|
|
27
27
|
"@mrrlin/schemas": "0.0.0",
|
|
28
|
-
"@mrrlin/
|
|
29
|
-
"@mrrlin/
|
|
28
|
+
"@mrrlin/tsconfig": "0.0.0",
|
|
29
|
+
"@mrrlin/wiki": "0.0.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@iarna/toml": "^2.2.5",
|