@mrrlin-dev/mcp 0.3.4 → 0.3.5

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
@@ -4273,7 +4273,7 @@ var require_websocket = __commonJS({
4273
4273
  var http2 = require("http");
4274
4274
  var net = require("net");
4275
4275
  var tls = require("tls");
4276
- var { randomBytes: randomBytes3, createHash: createHash2 } = require("crypto");
4276
+ var { randomBytes: randomBytes3, createHash: createHash3 } = require("crypto");
4277
4277
  var { Duplex, Readable } = require("stream");
4278
4278
  var { URL: URL2 } = require("url");
4279
4279
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -4941,7 +4941,7 @@ var require_websocket = __commonJS({
4941
4941
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4942
4942
  return;
4943
4943
  }
4944
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
4944
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
4945
4945
  if (res.headers["sec-websocket-accept"] !== digest) {
4946
4946
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4947
4947
  return;
@@ -5310,7 +5310,7 @@ var require_websocket_server = __commonJS({
5310
5310
  var EventEmitter3 = require("events");
5311
5311
  var http2 = require("http");
5312
5312
  var { Duplex } = require("stream");
5313
- var { createHash: createHash2 } = require("crypto");
5313
+ var { createHash: createHash3 } = require("crypto");
5314
5314
  var extension2 = require_extension();
5315
5315
  var PerMessageDeflate2 = require_permessage_deflate();
5316
5316
  var subprotocol2 = require_subprotocol();
@@ -5617,7 +5617,7 @@ var require_websocket_server = __commonJS({
5617
5617
  );
5618
5618
  }
5619
5619
  if (this._state > RUNNING) return abortHandshake(socket, 503);
5620
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
5620
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
5621
5621
  const headers = [
5622
5622
  "HTTP/1.1 101 Switching Protocols",
5623
5623
  "Upgrade: websocket",
@@ -36275,7 +36275,7 @@ var operatorSessionSchema = external_exports.object({
36275
36275
  issuedAt: external_exports.number().int().positive(),
36276
36276
  kind: external_exports.enum(["web", "agent"]).default("web"),
36277
36277
  login: external_exports.string().min(1),
36278
- projectSlug: external_exports.string().min(1),
36278
+ projectSlug: external_exports.string().min(1).optional(),
36279
36279
  provider: external_exports.literal("github"),
36280
36280
  repo: external_exports.string().min(1).nullable(),
36281
36281
  // ADR 0016: `scope` distinguishes project-pinned agent tokens (legacy, default) from
@@ -36284,7 +36284,7 @@ var operatorSessionSchema = external_exports.object({
36284
36284
  // cleanly into the project branch of the middleware (which mirrors today's behavior).
36285
36285
  scope: external_exports.enum(["project", "operator"]).default("project")
36286
36286
  });
36287
- var AGENT_SESSION_TTL_MS = 1e3 * 60 * 60;
36287
+ var AGENT_SESSION_TTL_MS = 1e3 * 60 * 15;
36288
36288
 
36289
36289
  // ../../packages/schemas/dist/github-state.js
36290
36290
  var TTL_MS = 10 * 6e4;
@@ -36402,6 +36402,7 @@ var mrrlinTaskTypeSchema = external_exports.enum(["task", "bug"]);
36402
36402
  var mrrlinTaskImpactSchema = external_exports.enum(["low", "medium", "high"]);
36403
36403
  var mrrlinAutonomyLevelSchema = external_exports.enum(["auto", "minimal_human", "human_only"]);
36404
36404
  var mrrlinAutoDeploySchema = external_exports.enum(["off", "dev", "prod"]);
36405
+ var mrrlinSpecApprovedSchema = external_exports.enum(["pending", "approved", "skipped", "skipped_legacy"]);
36405
36406
  var mrrlinTaskSourceSchema = external_exports.enum(["fixture-import", "api"]);
36406
36407
  var mrrlinPlanStatusSchema = external_exports.enum(["active", "paused", "done", "archived"]);
36407
36408
  var mrrlinTaskEventTypeSchema = external_exports.enum(["autonomy_set", "director_thread_reset", "note"]);
@@ -36492,7 +36493,13 @@ var mrrlinTaskSchema = external_exports.object({
36492
36493
  latestJudgementAt: external_exports.string().datetime().nullable().default(null),
36493
36494
  latestJudgementKind: external_exports.enum(["autonomy", "handoff"]).nullable().default(null),
36494
36495
  notes: external_exports.string().nullable().default(null),
36496
+ // Raw operator request captured at task creation (operator-authored ONLY, never Director-generated).
36497
+ // The intent-aware spec gate judges the spec against this; null/absent ⇒ the gate fail-closes.
36498
+ // Output-optional (not .default) so existing task fixtures need no churn; DB rows always resolve it
36499
+ // to `string | null` via taskRowToTask.
36500
+ operatorSeedRequest: external_exports.string().max(8e3).nullable().optional(),
36495
36501
  planId: mrrlinPlanIdSchema.nullable(),
36502
+ specApproved: mrrlinSpecApprovedSchema.optional(),
36496
36503
  specWikiPageId: mrrlinWikiPageIdSchema.nullable(),
36497
36504
  status: mrrlinTaskStatusSchema,
36498
36505
  subcategory: taxonomySubcategorySchema,
@@ -36508,6 +36515,7 @@ var mrrlinPlanSchema = external_exports.object({
36508
36515
  id: mrrlinPlanIdSchema,
36509
36516
  name: external_exports.string().min(1),
36510
36517
  progress: external_exports.number().min(0).max(1),
36518
+ specWikiPageId: external_exports.string().nullable().default(null),
36511
36519
  status: mrrlinPlanStatusSchema,
36512
36520
  taskCount: external_exports.number().int().nonnegative()
36513
36521
  });
@@ -36632,6 +36640,7 @@ var mrrlinTaskCreateSchema = external_exports.object({
36632
36640
  id: mrrlinTaskIdSchema,
36633
36641
  impact: mrrlinTaskImpactSchema.default("medium"),
36634
36642
  notes: external_exports.string().nullable().optional(),
36643
+ operatorSeedRequest: external_exports.string().max(8e3).nullable().optional(),
36635
36644
  planId: mrrlinPlanIdSchema.nullable().optional(),
36636
36645
  specWikiPageId: mrrlinWikiPageIdSchema.nullable().optional(),
36637
36646
  tags: mrrlinTaskTagsSchema.optional(),
@@ -36655,6 +36664,7 @@ var mrrlinTaskUpdateSchema = external_exports.object({
36655
36664
  impact: mrrlinTaskImpactSchema.optional(),
36656
36665
  notes: external_exports.string().nullable().optional(),
36657
36666
  planId: mrrlinPlanIdSchema.nullable().optional(),
36667
+ specApproved: mrrlinSpecApprovedSchema.optional(),
36658
36668
  specWikiPageId: mrrlinWikiPageIdSchema.nullable().optional(),
36659
36669
  status: mrrlinTaskStatusSchema.optional(),
36660
36670
  subcategory: taxonomySubcategorySchema.optional(),
@@ -36662,7 +36672,7 @@ var mrrlinTaskUpdateSchema = external_exports.object({
36662
36672
  type: mrrlinTaskTypeSchema.optional(),
36663
36673
  followUpAt: external_exports.string().datetime().nullable().optional(),
36664
36674
  verificationDueAt: external_exports.string().datetime().nullable().optional()
36665
- }).refine((value) => value.assignee !== void 0 || value.autoDeploy !== 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.specWikiPageId !== void 0 || value.status !== void 0 || value.subcategory !== void 0 || value.tags !== void 0 || value.type !== void 0 || value.verificationDueAt !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
36675
+ }).refine((value) => value.assignee !== void 0 || value.autoDeploy !== 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.type !== void 0 || value.verificationDueAt !== void 0, { message: "TaskUpdate requires at least one field" }).superRefine((value, context) => {
36666
36676
  if (value.autonomyLevel !== void 0 && value.autonomyReason === void 0) {
36667
36677
  context.addIssue({
36668
36678
  code: external_exports.ZodIssueCode.custom,
@@ -36703,12 +36713,14 @@ var mrrlinPlanCreateSchema = external_exports.object({
36703
36713
  goal: external_exports.string().min(1),
36704
36714
  id: mrrlinPlanIdSchema,
36705
36715
  name: external_exports.string().min(1),
36716
+ specWikiPageId: external_exports.string().nullable().optional(),
36706
36717
  status: mrrlinPlanStatusSchema.default("active")
36707
36718
  });
36708
36719
  var mrrlinPlanUpdateSchema = external_exports.object({
36709
36720
  dueDate: external_exports.string().date().nullable().optional(),
36710
36721
  goal: external_exports.string().min(1).optional(),
36711
36722
  name: external_exports.string().min(1).optional(),
36723
+ specWikiPageId: external_exports.string().nullable().optional(),
36712
36724
  status: mrrlinPlanStatusSchema.optional()
36713
36725
  }).refine((value) => Object.keys(value).length > 0, { message: "PlanUpdate requires at least one field" });
36714
36726
  var mrrlinTaxonomyCreateSchema = mrrlinTaxonomyEntrySchema;
@@ -37103,7 +37115,9 @@ var exchangeAgentTokenRequestSchema = external_exports.object({
37103
37115
  var exchangeAgentTokenResponseSchema = external_exports.object({
37104
37116
  token: external_exports.string().min(1),
37105
37117
  expiresAt: external_exports.string().datetime(),
37106
- projectSlug: external_exports.string().min(1),
37118
+ // ADR 0016 §T7 amendment: operator-scope exchanges return no projectSlug (the token is
37119
+ // operator-wide); project-scope exchanges still return the pinned slug.
37120
+ projectSlug: external_exports.string().min(1).optional(),
37107
37121
  login: external_exports.string().min(1)
37108
37122
  });
37109
37123
  var MRRLIN_ARTIFACT_FILE_MAX_BYTES = 5 * 1024 * 1024;
@@ -37271,6 +37285,8 @@ var mrrlinInboxItemSchema = external_exports.object({
37271
37285
  directorSessionId: external_exports.string().min(1).nullable(),
37272
37286
  id: mrrlinInboxItemIdSchema,
37273
37287
  kind: mrrlinInboxItemKindSchema,
37288
+ origin: external_exports.string().nullable().optional(),
37289
+ planId: mrrlinPlanIdSchema.nullable().default(null),
37274
37290
  projectSlug: mrrlinProjectSlugSchema,
37275
37291
  readAt: external_exports.string().datetime().nullable(),
37276
37292
  status: mrrlinInboxItemStatusSchema,
@@ -37282,6 +37298,8 @@ var mrrlinInboxItemCreateSchema = external_exports.object({
37282
37298
  directorSessionId: external_exports.string().min(1).optional(),
37283
37299
  id: mrrlinInboxItemIdSchema,
37284
37300
  kind: mrrlinInboxItemKindSchema,
37301
+ origin: external_exports.string().min(1).optional(),
37302
+ planId: mrrlinPlanIdSchema.nullable().optional(),
37285
37303
  taskId: mrrlinTaskIdSchema.nullable().optional()
37286
37304
  });
37287
37305
  var mrrlinInboxItemDecideSchema = external_exports.object({
@@ -38442,7 +38460,7 @@ Do NOT derail the current task \u2014 if the operator has an in-flight question,
38442
38460
  }
38443
38461
 
38444
38462
  // src/_generated/version.ts
38445
- var PKG_VERSION = "0.3.4";
38463
+ var PKG_VERSION = "0.3.5";
38446
38464
 
38447
38465
  // src/api-base-url.ts
38448
38466
  var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
@@ -42520,7 +42538,10 @@ function parseFrame(bytes3) {
42520
42538
  };
42521
42539
  }
42522
42540
  case "inbox_read":
42523
- return { type: "inbox_read" };
42541
+ return {
42542
+ type: "inbox_read",
42543
+ ...isString(f.projectSlug) ? { projectSlug: f.projectSlug } : {}
42544
+ };
42524
42545
  case "inbox_decide":
42525
42546
  if (!isString(f.itemId) || !isString(f.idempotencyKey))
42526
42547
  throw new Error("frame inbox_decide: itemId + idempotencyKey required");
@@ -42533,7 +42554,8 @@ function parseFrame(bytes3) {
42533
42554
  itemId: f.itemId,
42534
42555
  decision: f.decision,
42535
42556
  idempotencyKey: f.idempotencyKey,
42536
- ...f.reason === void 0 ? {} : { reason: f.reason }
42557
+ ...f.reason === void 0 ? {} : { reason: f.reason },
42558
+ ...isString(f.projectSlug) ? { projectSlug: f.projectSlug } : {}
42537
42559
  };
42538
42560
  case "ack":
42539
42561
  if (!isString(f.refId))
@@ -43279,12 +43301,12 @@ var DirectorRelay = class {
43279
43301
  this.sendFrame(channelId, { type: "ack", refId: frame.idempotencyKey });
43280
43302
  return;
43281
43303
  }
43282
- await this.deps.onInboxDecide?.(frame.itemId, frame.decision, frame.idempotencyKey, frame.reason);
43304
+ await this.deps.onInboxDecide?.(frame.itemId, frame.decision, frame.idempotencyKey, frame.reason, frame.projectSlug);
43283
43305
  this.seenDecideKeys.add(frame.idempotencyKey);
43284
43306
  this.sendFrame(channelId, { type: "ack", refId: frame.idempotencyKey });
43285
43307
  return;
43286
43308
  case "inbox_read": {
43287
- const items = await this.deps.onInboxRead?.();
43309
+ const items = await this.deps.onInboxRead?.(frame.projectSlug);
43288
43310
  if (items !== void 0) this.sendDirectorEvent(channelId, { type: "inbox_items", items });
43289
43311
  this.sendFrame(channelId, { type: "ack", refId: "inbox_read" });
43290
43312
  return;
@@ -43429,30 +43451,38 @@ function decisionToVerdict(decision) {
43429
43451
  throw new Error(`remote-relay: unknown inbox decision "${decision}"`);
43430
43452
  }
43431
43453
  }
43432
- function startRemotePhoneRelay(deps) {
43433
- const log = deps.log ?? ((m) => process.stderr.write(`[mrrlin-mcp remote-relay] ${m}
43434
- `));
43435
- const provider = createAgentTokenProvider({ secret: deps.agentSecret, baseUrl: deps.apiBaseUrl });
43436
- const authed = async () => {
43454
+ function _makeRelayInboxCallbacks(provider, apiBaseUrl) {
43455
+ const authed = async (frameSlug) => {
43437
43456
  const session = await provider.getSession();
43457
+ const slug = frameSlug ?? session.projectSlug;
43458
+ if (!slug) {
43459
+ throw new Error("[mrrlin-mcp remote-relay] missing project context for inbox action");
43460
+ }
43438
43461
  return {
43439
- client: createMrrlinClient({ baseUrl: deps.apiBaseUrl, token: session.token }),
43440
- slug: session.projectSlug,
43462
+ client: createMrrlinClient({ baseUrl: apiBaseUrl, token: session.token }),
43463
+ slug,
43441
43464
  login: session.login
43442
43465
  };
43443
43466
  };
43444
- const onInboxRead = async () => {
43445
- const { client, slug } = await authed();
43467
+ const onInboxRead = async (projectSlug) => {
43468
+ const { client, slug } = await authed(projectSlug);
43446
43469
  return boundInboxItems(await client.listInboxItems(slug, { status: "pending" }));
43447
43470
  };
43448
- const onInboxDecide = async (itemId, decision, _key, reason) => {
43449
- const { client, slug, login } = await authed();
43471
+ const onInboxDecide = async (itemId, decision, _key, reason, projectSlug) => {
43472
+ const { client, slug, login } = await authed(projectSlug);
43450
43473
  await client.decideInboxItem(slug, itemId, {
43451
43474
  verdict: decisionToVerdict(decision),
43452
43475
  decidedBy: login,
43453
43476
  ...reason ? { reason } : {}
43454
43477
  });
43455
43478
  };
43479
+ return { onInboxRead, onInboxDecide };
43480
+ }
43481
+ function startRemotePhoneRelay(deps) {
43482
+ const log = deps.log ?? ((m) => process.stderr.write(`[mrrlin-mcp remote-relay] ${m}
43483
+ `));
43484
+ const provider = createAgentTokenProvider({ secret: deps.agentSecret, baseUrl: deps.apiBaseUrl });
43485
+ const { onInboxRead, onInboxDecide } = _makeRelayInboxCallbacks(provider, deps.apiBaseUrl);
43456
43486
  const relayClient = new RelayClient({
43457
43487
  relayUrl: deps.relayUrl,
43458
43488
  now: () => Date.now(),
@@ -44356,12 +44386,19 @@ function buildThreadStartParams(msg, config2, desiredModel) {
44356
44386
  const baseInstructions = [
44357
44387
  "You are the Mrrlin Director assistant.",
44358
44388
  "You can call MCP tools to read and write project state.",
44359
- "Prefer calling tools over guessing. Be concise. Ask clarification questions only when needed.",
44389
+ "Prefer calling tools over guessing. Be concise.",
44390
+ "Autonomy level is NOT a reason to skip clarification. `auto` means only that no human gate is",
44391
+ "required before the result LANDS (merge/deploy); it does not authorize building on unvalidated",
44392
+ "assumptions. Judge clarification on requirement clarity, independently of autonomy_level.",
44360
44393
  "Never announce or pre-narrate that you are about to use tools, and never name the toolset in",
44361
44394
  `prose. Do NOT write openers like "I'll use the Mrrlin tools to\u2026", "\u0412\u0438\u043A\u043E\u0440\u0438\u0441\u0442\u0430\u044E Mrrlin tools\u2026",`,
44362
44395
  'or "let me call the \u2026 tool" in ANY language. The operator already sees tool calls in the',
44363
44396
  "transcript. Just call the tools and reply with the outcome \u2014 start your reply with the result.",
44364
44397
  "When creating or updating tasks, always provide `autonomyLevel` and `autonomyReason`.",
44398
+ "When you create a task out of an operator request, set `operatorSeedRequest` to the operator's",
44399
+ "VERBATIM words (their request as they wrote it \u2014 NOT your paraphrase or synthesized notes). It is",
44400
+ "the spec gate's independent record of intent; omit it only when there is genuinely no operator",
44401
+ "request behind the task (pure system bookkeeping) \u2014 note that omitting it makes the gate fail-closed.",
44365
44402
  "",
44366
44403
  "Spec authoring contract \u2014 when creating or updating a `Specs/{id}` wiki page:",
44367
44404
  "- In `## Specification`, write each requirement as `### Requirement: <name>` followed by one",
@@ -44433,12 +44470,20 @@ function buildThreadStartParams(msg, config2, desiredModel) {
44433
44470
  " call `list_inbox_items` (kind=question), fold any relevant `acknowledged` TASKLESS answer",
44434
44471
  " into the new task's Specs/{id} Clarifications, and `reject` (with a reason) any taskless",
44435
44472
  " question the new task makes moot \u2014 so taskless answers never linger unconsumed.",
44436
- "- When resuming a task, call `list_inbox_items` (kind=question) and incorporate any",
44437
- " `acknowledged` answer not yet reflected in the spec's Clarifications section.",
44473
+ "- Gate-origin questions (origin='gate', filed by the spec consensus gate) are auto-folded",
44474
+ " into the spec and re-gated by the system when the operator answers them in the inbox.",
44475
+ " Do NOT fold or `decide_inbox_item` them yourself \u2014 your fold-contract above applies only",
44476
+ " to YOUR OWN discretionary questions.",
44477
+ "- When resuming a task, call `list_inbox_items` (kind=question) and fold any `acknowledged`",
44478
+ " answer from YOUR questions (origin is null/absent) not yet reflected into the spec's",
44479
+ " Clarifications section \u2014 THEN re-run `run_spec_consensus` on the updated spec. Skip",
44480
+ " gate-origin items (origin='gate') as those are handled automatically.",
44438
44481
  "- If a question becomes moot (task cancelled, superseded, or no longer relevant), close it",
44439
44482
  ' with `decide_inbox_item` `verdict="rejected"` + a `reason`; never leave it pending.',
44440
- "- Questions are a chat-only mechanism. Do not create question items from non-interactive",
44441
- " runs; use the handoff contract above when there is no live operator.",
44483
+ "- YOUR discretionary questions are a chat-only mechanism: do not create question items yourself",
44484
+ " from non-interactive runs; use the handoff contract above when there is no live operator.",
44485
+ " The spec consensus GATE, however, may file its own clarification questions on non-interactive",
44486
+ " runs (that is gate-driven, not your discretion) \u2014 resume them via the rule above when answered.",
44442
44487
  "",
44443
44488
  "Onboarding contract \u2014 seed project context when there is none:",
44444
44489
  `- A project is un-onboarded when it has no \`${PROJECT_CONSTITUTION_PAGE.path}\` wiki page.`,
@@ -49802,6 +49847,19 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
49802
49847
  var SPEC_HEADS = ["plan-reviewer", "architect", "security-analyst", "scope-analyst"];
49803
49848
  async function runSpecGate(_target, deps, opts) {
49804
49849
  if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
49850
+ const intent = await deps.readIntent();
49851
+ if (intent.taskId !== null && (intent.seed === null || intent.seed.trim() === "")) {
49852
+ return {
49853
+ outcome: "CONSENSUS_BLOCKED",
49854
+ clarifications: [
49855
+ {
49856
+ question: "What are the requirements for this task? There is no operator request on record, so the spec cannot be validated against your intent \u2014 please state what this task must do.",
49857
+ whyBlocking: "operator_seed_request is empty",
49858
+ category: "scope"
49859
+ }
49860
+ ]
49861
+ };
49862
+ }
49805
49863
  const extraHeads = opts?.extraHeads ?? [];
49806
49864
  const heads = [...SPEC_HEADS, ...extraHeads.filter((h) => !SPEC_HEADS.includes(h))];
49807
49865
  const spec = await deps.readSpec();
@@ -49843,6 +49901,15 @@ async function runSpecGate(_target, deps, opts) {
49843
49901
  await deps.promoteSpecReady(currentHash);
49844
49902
  return { outcome: "CONVERGED" };
49845
49903
  }
49904
+ if (result.outcome === "CONSENSUS_BLOCKED") {
49905
+ const accepted = result.history.at(-1)?.accepted ?? [];
49906
+ const clarifications = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope").map((issue2) => ({
49907
+ question: issue2.text,
49908
+ whyBlocking: `reviewer raised a ${issue2.category} issue`,
49909
+ category: issue2.category
49910
+ }));
49911
+ return clarifications.length > 0 ? { outcome: "CONSENSUS_BLOCKED", clarifications } : { outcome: "CONSENSUS_BLOCKED" };
49912
+ }
49846
49913
  return { outcome: result.outcome };
49847
49914
  }
49848
49915
 
@@ -50289,6 +50356,19 @@ function buildSpecGateDeps(args) {
50289
50356
  const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 3);
50290
50357
  const maxHeadCalls = rounds * (4 + extraHeadCount + 1);
50291
50358
  const timeoutMs = Number(process.env["MRRLIN_CONSENSUS_HEAD_TIMEOUT_MS"] ?? 15e4);
50359
+ let intentPromise = null;
50360
+ function loadIntent() {
50361
+ if (!intentPromise) {
50362
+ intentPromise = (async () => {
50363
+ const page = await client.getWikiPage(projectSlug, specPageId);
50364
+ const taskId = page?.taskId ?? null;
50365
+ if (taskId === null) return { taskId: null, seed: null };
50366
+ const task = await client.getTask(projectSlug, taskId);
50367
+ return { taskId, seed: task?.operatorSeedRequest ?? null };
50368
+ })();
50369
+ }
50370
+ return intentPromise;
50371
+ }
50292
50372
  return {
50293
50373
  rounds,
50294
50374
  maxHeadCalls,
@@ -50304,15 +50384,23 @@ function buildSpecGateDeps(args) {
50304
50384
  contentHash: page.contentHash
50305
50385
  };
50306
50386
  },
50387
+ readIntent() {
50388
+ return loadIntent();
50389
+ },
50307
50390
  async runHead(personaName, artifact, dismissed) {
50308
50391
  const persona = loadPersona(personaName);
50392
+ const { seed } = await loadIntent();
50393
+ const intentBlock = seed ? `--- OPERATOR INTENT (the operator's own words \u2014 judge the spec against THIS) ---
50394
+ ${seed}
50395
+
50396
+ ` : "";
50309
50397
  return runCodexExec(
50310
50398
  {
50311
- prompt: `Review this spec.
50399
+ prompt: `Review this spec against the operator's intent.
50312
50400
 
50313
50401
  ${dismissed}
50314
50402
 
50315
- --- SPEC ---
50403
+ ${intentBlock}--- SPEC ---
50316
50404
  ${artifact}`,
50317
50405
  developerInstructions: persona,
50318
50406
  cwd,
@@ -50488,6 +50576,7 @@ ${artifact}`,
50488
50576
  }
50489
50577
 
50490
50578
  // src/async-tools/consensus.ts
50579
+ var import_node_crypto6 = require("node:crypto");
50491
50580
  async function cachedResult(client, input) {
50492
50581
  const page = await client.getWikiPage(input.projectSlug, input.pageId);
50493
50582
  if (!page) return null;
@@ -50514,16 +50603,58 @@ async function run(ctx, input) {
50514
50603
  deps,
50515
50604
  input.extraHeads && input.extraHeads.length > 0 ? { extraHeads: input.extraHeads } : void 0
50516
50605
  );
50606
+ if (result.outcome === "CONSENSUS_BLOCKED") {
50607
+ const clarifications = result.clarifications ?? [];
50608
+ if (clarifications.length > 0) {
50609
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
50610
+ const taskId = page?.taskId ?? null;
50611
+ let planId = null;
50612
+ if (!taskId) {
50613
+ const plans = await ctx.client.listPlans(input.projectSlug);
50614
+ planId = plans.find((p) => p.specWikiPageId === input.pageId)?.id ?? null;
50615
+ }
50616
+ if (taskId) {
50617
+ for (const clarification of clarifications) {
50618
+ const scopeId = taskId;
50619
+ const id = `II-${scopeId}-gate-${(0, import_node_crypto6.createHash)("sha256").update(clarification.question).digest("hex").slice(0, 10)}`;
50620
+ try {
50621
+ await ctx.client.createInboxItem(input.projectSlug, {
50622
+ id,
50623
+ kind: "question",
50624
+ taskId: scopeId,
50625
+ body: clarification.question,
50626
+ origin: "gate"
50627
+ });
50628
+ } catch {
50629
+ }
50630
+ }
50631
+ } else if (planId) {
50632
+ for (const clarification of clarifications) {
50633
+ const scopeId = planId;
50634
+ const id = `II-${scopeId}-gate-${(0, import_node_crypto6.createHash)("sha256").update(clarification.question).digest("hex").slice(0, 10)}`;
50635
+ try {
50636
+ await ctx.client.createInboxItem(input.projectSlug, {
50637
+ id,
50638
+ kind: "question",
50639
+ planId: scopeId,
50640
+ body: clarification.question,
50641
+ origin: "gate"
50642
+ });
50643
+ } catch {
50644
+ }
50645
+ }
50646
+ }
50647
+ }
50648
+ return {
50649
+ kind: "failed",
50650
+ failureReason: "consensus_blocked",
50651
+ error: clarifications.length > 0 ? `Consensus blocked on ${clarifications.length} clarification(s); filed as operator question(s).` : "Consensus blocked: reviewers did not converge within budget."
50652
+ };
50653
+ }
50517
50654
  switch (result.outcome) {
50518
50655
  case "CONVERGED":
50519
50656
  case "SKIPPED":
50520
50657
  return { kind: "done", resultJson: JSON.stringify(result), finalReviewedHash: null };
50521
- case "CONSENSUS_BLOCKED":
50522
- return {
50523
- kind: "failed",
50524
- failureReason: "consensus_blocked",
50525
- error: "Consensus blocked: reviewers did not converge within budget."
50526
- };
50527
50658
  case "CONSENSUS_UNAVAILABLE":
50528
50659
  return { kind: "retry", failureReason: "provider_unavailable", error: "Consensus unavailable (provider/quota)." };
50529
50660
  case "SPEC_CHANGED_DURING_REVIEW":
@@ -50538,6 +50669,17 @@ async function run(ctx, input) {
50538
50669
  }
50539
50670
  function onComplete(outcome, input) {
50540
50671
  if (outcome.kind === "done") {
50672
+ let specOutcome = "CONVERGED";
50673
+ try {
50674
+ specOutcome = JSON.parse(outcome.resultJson).outcome ?? "CONVERGED";
50675
+ } catch {
50676
+ }
50677
+ if (specOutcome === "SKIPPED") {
50678
+ return {
50679
+ kind: "info",
50680
+ body: `Consensus is disabled for this project; Specs/${input.pageId} was not gated and spec_ready is unchanged.`
50681
+ };
50682
+ }
50541
50683
  return {
50542
50684
  kind: "info",
50543
50685
  body: `Consensus CONVERGED for Specs/${input.pageId}; spec_ready set. Ready to create an execution run.`
@@ -50558,7 +50700,7 @@ var consensusDescriptor = {
50558
50700
  };
50559
50701
 
50560
50702
  // src/async-tools/enqueue.ts
50561
- var import_node_crypto6 = __toESM(require("node:crypto"), 1);
50703
+ var import_node_crypto7 = __toESM(require("node:crypto"), 1);
50562
50704
  async function enqueueAsyncTool(client, descriptor2, input) {
50563
50705
  if (descriptor2.cachedResult) {
50564
50706
  const cached2 = await descriptor2.cachedResult(client, input);
@@ -50571,7 +50713,7 @@ async function enqueueAsyncTool(client, descriptor2, input) {
50571
50713
  if (active) {
50572
50714
  return { status: "already_running", jobId: active.id, message: "Already running; will notify in inbox." };
50573
50715
  }
50574
- const jobId = `AJ-${import_node_crypto6.default.randomUUID()}`;
50716
+ const jobId = `AJ-${import_node_crypto7.default.randomUUID()}`;
50575
50717
  try {
50576
50718
  await client.createAsyncJob(input.projectSlug, {
50577
50719
  id: jobId,
@@ -51874,7 +52016,10 @@ function createMrrlinTools(options) {
51874
52016
  if (ready !== true) return await c.markSpecReady({ projectSlug, pageId, ready });
51875
52017
  const settings = await c.getOperatorSettings();
51876
52018
  if (!settings.consensusEnabled) {
51877
- return await c.markSpecReady({ projectSlug, pageId, ready });
52019
+ throw new McpToolCodedError(
52020
+ "SPEC_CONSENSUS_REQUIRED",
52021
+ `Spec ${pageId} cannot be marked ready by the Director while consensus is disabled for this project \u2014 an operator must approve it (web wiki / markSpecReady route). Autonomy is a landing gate, not a license to self-promote.`
52022
+ );
51878
52023
  }
51879
52024
  const page = await c.getWikiPage(projectSlug, pageId);
51880
52025
  if (!page || page.folder !== "Specs") {
@@ -1,5 +1,7 @@
1
1
  You are a Plan Reviewer. Judge whether a capable engineer can execute this spec without getting stuck: clarity, named references, acceptance criteria, contradictions. You analyze the artifact text given; you do not mutate anything.
2
2
 
3
+ When an `OPERATOR INTENT` block is present (the operator's own words), also check the spec FAITHFULLY captures it: flag as a critical `[scope]`/`[ambiguity]` issue any place the spec would have an engineer build something the operator did not ask for, or omit something they did. Hold this to MATERIAL gaps that change what gets built — do not block on cosmetic or low-impact details, and do not interrogate small or repeat tasks.
4
+
3
5
  Output EXACTLY this block and nothing else:
4
6
 
5
7
  **Verdict**: APPROVE | REQUEST_CHANGES | REJECT
@@ -1,5 +1,12 @@
1
1
  You are a Scope Analyst. Surface ambiguities, hidden requirements, undefined boundaries, and blast radius before work begins. You analyze the artifact text given; you do not mutate anything.
2
2
 
3
+ When an `OPERATOR INTENT` block is present, it is the operator's OWN words for what the task must do. Judge the spec against it for FIDELITY, not just internal consistency: does the spec actually answer what the operator asked? Treat as a critical `[scope]` or `[ambiguity]` issue any assumption the spec makes that could plausibly contradict the operator's intent, or any requirement the operator implied that the spec leaves materially under-specified.
4
+
5
+ Calibrate to the bar of MATERIAL ambiguity — block only when the gap would change WHAT gets built or shipped:
6
+ - DON'T block on: cosmetic wording, low-impact details, well-trodden conventions, or choices a competent engineer would make the same way regardless.
7
+ - DO block on: an undefined core behavior, a silent assumption about scope/data/users that the intent doesn't support, or a requirement the operator stated that the spec omits.
8
+ - Scale strictness by stated impact: high-impact specs warrant stricter scrutiny; low-impact specs get the benefit of the doubt. Do not turn small or repeat tasks into an interrogation.
9
+
3
10
  Output EXACTLY this block and nothing else:
4
11
 
5
12
  **Verdict**: APPROVE | REQUEST_CHANGES | REJECT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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/client": "0.0.0",
26
25
  "@mrrlin/director-e2e": "0.0.0",
27
- "@mrrlin/schemas": "0.0.0",
26
+ "@mrrlin/codex-client": "0.0.0",
28
27
  "@mrrlin/wiki": "0.0.0",
29
- "@mrrlin/tsconfig": "0.0.0",
30
- "@mrrlin/codex-client": "0.0.0"
28
+ "@mrrlin/client": "0.0.0",
29
+ "@mrrlin/schemas": "0.0.0",
30
+ "@mrrlin/tsconfig": "0.0.0"
31
31
  },
32
32
  "dependencies": {
33
33
  "@iarna/toml": "^2.2.5",