@mrrlin-dev/mcp 0.3.59 → 0.3.60

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
@@ -21833,136 +21833,6 @@ async function acquireThread(codex, opts) {
21833
21833
  return { threadId: newId, resumed: false };
21834
21834
  }
21835
21835
 
21836
- // ../../packages/codex-client/dist/render-context-bundle.js
21837
- var DEFAULT_SPEC_BUDGET_CHARS = 6e3;
21838
- var DEFAULT_ARTIFACT_PAYLOAD_BUDGET_CHARS = 400;
21839
- function renderContextBundle(input) {
21840
- const sections = [];
21841
- sections.push(renderTaskSummary(input.task));
21842
- sections.push(renderSpec(input.specMarkdown, input.compact));
21843
- sections.push(renderRunHistory(input.runs, input.openRunId));
21844
- sections.push(renderOpenRun(input.openRunId));
21845
- sections.push(renderArtifacts(input.artifacts, input.omittedArtifactCount ?? 0, input.compact));
21846
- sections.push(renderAutonomyEvents(input.autonomyEvents));
21847
- return sections.join("\n\n");
21848
- }
21849
- function renderTaskSummary(task) {
21850
- const lines2 = [
21851
- "## Task",
21852
- `id: ${task.id}`,
21853
- `title: ${task.title}`,
21854
- `autonomy: ${task.autonomyLevel} (${task.autonomyReason})`
21855
- ];
21856
- if (task.notes)
21857
- lines2.push(`description: ${task.notes}`);
21858
- if (task.latestJudgement) {
21859
- lines2.push(`latest judgement (${task.latestJudgementKind ?? "unknown"}): ${task.latestJudgement}`);
21860
- }
21861
- return lines2.join("\n");
21862
- }
21863
- function renderSpec(specMarkdown, compact) {
21864
- if (!specMarkdown)
21865
- return "## Spec\n(none)";
21866
- if (!compact)
21867
- return `## Spec
21868
- ${specMarkdown}`;
21869
- const budget = compact.specBudgetChars ?? DEFAULT_SPEC_BUDGET_CHARS;
21870
- if (specMarkdown.length <= budget)
21871
- return `## Spec
21872
- ${specMarkdown}`;
21873
- return `## Spec
21874
- ${compactSpecMarkdown(specMarkdown, budget, compact.projectSlug ?? null, compact.specWikiPageId ?? null)}`;
21875
- }
21876
- function compactSpecMarkdown(markdown, totalBudget, projectSlug, specWikiPageId) {
21877
- const lines2 = markdown.split(/\r?\n/);
21878
- const sections = [{ heading: "", body: [] }];
21879
- for (const line of lines2) {
21880
- if (/^#{1,6}\s/.test(line)) {
21881
- sections.push({ heading: line, body: [] });
21882
- } else {
21883
- sections[sections.length - 1]?.body.push(line);
21884
- }
21885
- }
21886
- const sectionCount = Math.max(1, sections.length);
21887
- const perSectionBudget = Math.max(120, Math.floor(totalBudget / sectionCount));
21888
- const out = [];
21889
- for (const section of sections) {
21890
- const bodyText = section.body.join("\n").replace(/^\s+|\s+$/g, "");
21891
- if (section.heading)
21892
- out.push(section.heading);
21893
- if (!bodyText)
21894
- continue;
21895
- if (bodyText.length <= perSectionBudget) {
21896
- out.push(bodyText);
21897
- } else {
21898
- const kept = bodyText.slice(0, perSectionBudget);
21899
- const omitted = bodyText.length - perSectionBudget;
21900
- out.push(`${kept}
21901
- \u2026(${omitted} chars omitted; retrieve full spec for the rest)`);
21902
- }
21903
- }
21904
- if (projectSlug && specWikiPageId) {
21905
- out.push("");
21906
- out.push(`> Compact spec. Source: mrrlin://wiki/${projectSlug}/${specWikiPageId} \u2014 call retrieve_context with this handle before any irreversible decision.`);
21907
- } else {
21908
- out.push("");
21909
- out.push("> Compact spec. Retrieve the Specs/{id} wiki page via get_wiki_page before any irreversible decision.");
21910
- }
21911
- return out.join("\n");
21912
- }
21913
- function renderRunHistory(runs, openRunId) {
21914
- if (runs.length === 0)
21915
- return "## Execution-run history\n(none)";
21916
- const lines2 = ["## Execution-run history"];
21917
- for (const run3 of runs) {
21918
- const marker = run3.id === openRunId ? " (open)" : "";
21919
- lines2.push(`- ${run3.id} [${run3.status}] created ${run3.createdAt}${marker}`);
21920
- }
21921
- return lines2.join("\n");
21922
- }
21923
- function renderOpenRun(openRunId) {
21924
- return `## Open run
21925
- ${openRunId ?? "(none)"}`;
21926
- }
21927
- function renderArtifacts(artifacts, omittedCount, compact) {
21928
- if (artifacts.length === 0 && omittedCount === 0)
21929
- return "## Artifacts\n(none)";
21930
- const lines2 = ["## Artifacts"];
21931
- if (omittedCount > 0) {
21932
- lines2.push(`(+${omittedCount} older artifact${omittedCount === 1 ? "" : "s"} omitted)`);
21933
- }
21934
- let truncatedAny = false;
21935
- for (const artifact of artifacts) {
21936
- if (artifact.kind === "file") {
21937
- const p = artifact.payload;
21938
- if (p.filename && p.stableUrl) {
21939
- lines2.push(`- ${artifact.createdAt} [file] ${p.filename} (${p.class ?? "?"}, ${p.sizeBytes ?? "?"} bytes) ${p.stableUrl}`);
21940
- continue;
21941
- }
21942
- }
21943
- const payloadText = JSON.stringify(artifact.payload);
21944
- const budget = compact?.artifactPayloadBudgetChars ?? DEFAULT_ARTIFACT_PAYLOAD_BUDGET_CHARS;
21945
- const willTruncate = !!compact && payloadText.length > budget;
21946
- const rendered = willTruncate ? `${payloadText.slice(0, budget)}\u2026(${payloadText.length - budget} chars omitted)` : payloadText;
21947
- if (willTruncate)
21948
- truncatedAny = true;
21949
- lines2.push(`- ${artifact.createdAt} [${artifact.kind}] run=${artifact.runId} ${rendered}`);
21950
- }
21951
- if (compact && (omittedCount > 0 || truncatedAny)) {
21952
- lines2.push("> Compact artifacts. Use retrieve_context with a `mrrlin://run/{projectSlug}/{runId}/artifacts` handle (substitute the slug and run id from the lines above) to load a run's full artifact history.");
21953
- }
21954
- return lines2.join("\n");
21955
- }
21956
- function renderAutonomyEvents(events) {
21957
- if (events.length === 0)
21958
- return "## Autonomy history\n(none)";
21959
- const lines2 = ["## Autonomy history"];
21960
- for (const event of events) {
21961
- lines2.push(`- ${event.createdAt} ${event.autonomyLevel ?? "?"} (${event.autonomyReason ?? ""})`);
21962
- }
21963
- return lines2.join("\n");
21964
- }
21965
-
21966
21836
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
21967
21837
  var external_exports = {};
21968
21838
  __export(external_exports, {
@@ -36662,19 +36532,22 @@ var agentPoolResponseSchema = external_exports.object({
36662
36532
  });
36663
36533
 
36664
36534
  // ../../packages/schemas/dist/specConsensusReview.js
36535
+ var mrrlinSpecConsensusSuppressionEvidenceSchema = external_exports.record(external_exports.string(), external_exports.unknown()).nullable();
36665
36536
  var mrrlinSpecConsensusReviewSchema = external_exports.object({
36666
36537
  projectSlug: external_exports.string().min(1),
36667
36538
  specPageId: external_exports.string().min(1),
36668
36539
  contentHash: external_exports.string().min(1),
36669
36540
  outcome: external_exports.string().min(1),
36670
36541
  rounds: external_exports.number().int().nonnegative(),
36542
+ suppressionEvidence: mrrlinSpecConsensusSuppressionEvidenceSchema.default(null),
36671
36543
  createdAt: external_exports.string().datetime()
36672
36544
  });
36673
36545
  var mrrlinSpecConsensusReviewCreateSchema = external_exports.object({
36674
36546
  specPageId: external_exports.string().min(1),
36675
36547
  contentHash: external_exports.string().min(1),
36676
36548
  outcome: external_exports.string().min(1),
36677
- rounds: external_exports.number().int().nonnegative()
36549
+ rounds: external_exports.number().int().nonnegative(),
36550
+ suppressionEvidence: mrrlinSpecConsensusSuppressionEvidenceSchema.optional()
36678
36551
  }).strict();
36679
36552
  var specConsensusReviewResponseSchema = external_exports.object({ data: mrrlinSpecConsensusReviewSchema });
36680
36553
  var specConsensusReviewMaybeResponseSchema = external_exports.object({ data: mrrlinSpecConsensusReviewSchema.nullable() });
@@ -36750,6 +36623,97 @@ var mrrlinAsyncJobRequeueSchema = external_exports.object({
36750
36623
  staleSeconds: external_exports.number().int().min(60).max(86400).default(300)
36751
36624
  });
36752
36625
 
36626
+ // ../../packages/schemas/dist/memory-budget.js
36627
+ function evictionKey(item, idx) {
36628
+ const memClass = item.memoryClass === "historical" ? 0 : 1;
36629
+ const prec = item.precedence === null ? -4 : -item.precedence;
36630
+ const score = item.score ?? 0;
36631
+ return { memClass, prec, score, idx };
36632
+ }
36633
+ function compareEvictionKeys(a, b) {
36634
+ if (a.memClass !== b.memClass)
36635
+ return a.memClass - b.memClass;
36636
+ if (a.prec !== b.prec)
36637
+ return a.prec - b.prec;
36638
+ if (a.score !== b.score)
36639
+ return a.score - b.score;
36640
+ return a.idx - b.idx;
36641
+ }
36642
+ function sortByEvictionOrder(items) {
36643
+ const indexed = items.map((item, idx) => ({ item, key: evictionKey(item, idx) }));
36644
+ indexed.sort((a, b) => compareEvictionKeys(a.key, b.key));
36645
+ return indexed.map((x) => x.item);
36646
+ }
36647
+ function keepUnderCap(sorted, cap) {
36648
+ const kept = [];
36649
+ let used = 0;
36650
+ let i = sorted.length - 1;
36651
+ for (; i >= 0; i--) {
36652
+ const item = sorted[i];
36653
+ if (item === void 0)
36654
+ break;
36655
+ if (used + item.size <= cap) {
36656
+ kept.unshift(item);
36657
+ used += item.size;
36658
+ } else {
36659
+ break;
36660
+ }
36661
+ }
36662
+ const dropped = sorted.slice(0, i + 1);
36663
+ return { kept, dropped };
36664
+ }
36665
+ function applyBudget(items, spec) {
36666
+ const prec0Items = [];
36667
+ const restItems = [];
36668
+ for (const item of items) {
36669
+ if (item.precedence === 0) {
36670
+ prec0Items.push(item);
36671
+ } else {
36672
+ restItems.push(item);
36673
+ }
36674
+ }
36675
+ const sourceCaps = spec.sourceCaps ?? {};
36676
+ const sourceMap = /* @__PURE__ */ new Map();
36677
+ for (const item of restItems) {
36678
+ const list = sourceMap.get(item.source);
36679
+ if (list)
36680
+ list.push(item);
36681
+ else
36682
+ sourceMap.set(item.source, [item]);
36683
+ }
36684
+ const keptAfterSourceCap = [];
36685
+ const droppedAfterSourceCap = [];
36686
+ for (const [source, sourceItems] of sourceMap) {
36687
+ const cap = sourceCaps[source];
36688
+ if (cap === void 0) {
36689
+ keptAfterSourceCap.push(...sourceItems);
36690
+ } else {
36691
+ const sorted = sortByEvictionOrder(sourceItems);
36692
+ const { kept, dropped } = keepUnderCap(sorted, cap);
36693
+ keptAfterSourceCap.push(...kept);
36694
+ droppedAfterSourceCap.push(...dropped);
36695
+ }
36696
+ }
36697
+ const allEligible = keptAfterSourceCap;
36698
+ const sortedGlobal = sortByEvictionOrder(allEligible);
36699
+ const { kept: keptGlobal, dropped: droppedGlobal } = keepUnderCap(sortedGlobal, spec.totalBytes - totalSize(prec0Items));
36700
+ const finalKept = [...prec0Items, ...keptGlobal];
36701
+ const finalDropped = [...droppedAfterSourceCap, ...droppedGlobal];
36702
+ const trimmed = finalDropped.length > 0;
36703
+ const keptSize = totalSize(finalKept);
36704
+ const overflowBy = Math.max(0, keptSize - spec.totalBytes);
36705
+ const handles = [];
36706
+ for (const item of finalDropped) {
36707
+ if (item.handle && item.handle.trim() !== "") {
36708
+ handles.push(item.handle);
36709
+ }
36710
+ }
36711
+ return { kept: finalKept, dropped: finalDropped, handles, trimmed, overflowBy };
36712
+ }
36713
+ function totalSize(items) {
36714
+ return items.reduce((sum, i) => sum + i.size, 0);
36715
+ }
36716
+
36753
36717
  // ../../packages/schemas/dist/index.js
36754
36718
  var mrrlinTaskStatusSchema = external_exports.enum([
36755
36719
  "todo",
@@ -36981,6 +36945,7 @@ var mrrlinTaskEventSchema = external_exports.object({
36981
36945
  });
36982
36946
  var mrrlinWikiPageSchema = external_exports.object({
36983
36947
  contentHash: external_exports.string().default(""),
36948
+ createdAt: external_exports.string().datetime().nullable().default(null),
36984
36949
  folder: external_exports.string().min(1),
36985
36950
  id: mrrlinWikiPageIdSchema,
36986
36951
  markdownBody: external_exports.string().min(1),
@@ -36988,7 +36953,8 @@ var mrrlinWikiPageSchema = external_exports.object({
36988
36953
  preview: external_exports.string(),
36989
36954
  specReady: external_exports.boolean().default(false),
36990
36955
  taskId: mrrlinTaskIdSchema.nullable().default(null),
36991
- title: external_exports.string().min(1)
36956
+ title: external_exports.string().min(1),
36957
+ updatedAt: external_exports.string().datetime().nullable().default(null)
36992
36958
  });
36993
36959
  var mrrlinMarkSpecReadyRequestSchema = external_exports.object({
36994
36960
  ready: external_exports.boolean()
@@ -37112,14 +37078,12 @@ var mrrlinProjectSnapshotSchema = external_exports.object({
37112
37078
  taxonomy: external_exports.array(mrrlinTaxonomyEntrySchema).default([]),
37113
37079
  wikiPages: external_exports.array(mrrlinWikiPageSchema).default([])
37114
37080
  });
37115
- var mrrlinTaskCreateSchema = external_exports.object({
37081
+ var mrrlinTaskCreateShapeSchema = external_exports.object({
37116
37082
  // Auto-deploy design Part B: input-optional (NOT .default("off")) so createTask can tell
37117
37083
  // "caller omitted this" apart from "caller explicitly chose off" -- omitted falls through to
37118
37084
  // the project's default_auto_deploy; the tasks column itself still defaults to 'off' at the
37119
37085
  // SQL level for any direct-insert path that bypasses createTask entirely.
37120
37086
  autoDeploy: mrrlinAutoDeploySchema.optional(),
37121
- // Input-optional so existing createTask call sites need no churn; createTask fills 'code' for the
37122
- // INSERT (the pg column is NOT NULL, so a concrete value is required, not undefined→NULL).
37123
37087
  domain: mrrlinTaskDomainSchema.optional(),
37124
37088
  autonomyLevel: mrrlinAutonomyLevelSchema.default("human_review"),
37125
37089
  autonomyReason: external_exports.string().min(1).max(240).default("Needs human review before automation."),
@@ -37141,6 +37105,17 @@ var mrrlinTaskCreateSchema = external_exports.object({
37141
37105
  type: mrrlinTaskTypeSchema.default("task"),
37142
37106
  verificationDueAt: external_exports.string().datetime().nullable().optional()
37143
37107
  });
37108
+ function refineTaskCreateDomain(data, ctx) {
37109
+ if (data.domain === void 0 && data.parentTaskId == null) {
37110
+ ctx.addIssue({
37111
+ code: external_exports.ZodIssueCode.custom,
37112
+ path: ["domain"],
37113
+ message: "domain is required unless parentTaskId is set (a follow-up inherits its parent's domain)"
37114
+ });
37115
+ }
37116
+ }
37117
+ var mrrlinTaskCreateSchema = mrrlinTaskCreateShapeSchema.superRefine(refineTaskCreateDomain);
37118
+ var mrrlinTaskCreateWithoutIdSchema = mrrlinTaskCreateShapeSchema.omit({ id: true }).superRefine(refineTaskCreateDomain);
37144
37119
  var mrrlinTaskUpdateSchema = external_exports.object({
37145
37120
  actor: external_exports.string().min(1).optional(),
37146
37121
  assignee: mrrlinTaskAssigneeSchema.optional(),
@@ -37258,13 +37233,19 @@ var mrrlinWikiPageSearchQuerySchema = external_exports.object({
37258
37233
  q: external_exports.string().min(2)
37259
37234
  });
37260
37235
  var mrrlinWikiPageSearchResultSchema = external_exports.object({
37236
+ ageDays: external_exports.number().nullable(),
37261
37237
  folder: external_exports.string().min(1),
37262
37238
  id: mrrlinWikiPageIdSchema,
37239
+ // Task-relative; always null from a generic (non-task-scoped) search — resolved only where
37240
+ // a caller has task context (director-loop retrieve()), never at this layer or in the API (D14).
37241
+ memoryClass: external_exports.enum(["normative", "historical"]),
37263
37242
  path: external_exports.string().min(1),
37243
+ precedence: external_exports.union([external_exports.literal(0), external_exports.literal(1), external_exports.literal(2), external_exports.literal(3)]).nullable(),
37264
37244
  preview: external_exports.string(),
37265
37245
  rank: external_exports.number(),
37266
37246
  snippet: external_exports.string(),
37267
- title: external_exports.string().min(1)
37247
+ title: external_exports.string().min(1),
37248
+ updatedAt: external_exports.string().datetime().nullable()
37268
37249
  });
37269
37250
  var wikiKnowledgeChunkSchema = external_exports.object({
37270
37251
  chunk_text: external_exports.string().min(1),
@@ -38090,6 +38071,210 @@ var inboxItemsMarkReadResponseSchema = external_exports.object({
38090
38071
  var inboxItemResponseSchema = external_exports.object({ data: mrrlinInboxItemSchema });
38091
38072
  var inboxItemListResponseSchema = external_exports.object({ data: external_exports.array(mrrlinInboxItemSchema) });
38092
38073
 
38074
+ // ../../packages/codex-client/dist/render-context-bundle.js
38075
+ var DEFAULT_SPEC_BUDGET_CHARS = 6e3;
38076
+ var DEFAULT_ARTIFACT_PAYLOAD_BUDGET_CHARS = 400;
38077
+ function renderContextBundle(input) {
38078
+ const sections = [];
38079
+ sections.push(renderTaskSummary(input.task));
38080
+ sections.push(renderSpec(input.specMarkdown, input.compact));
38081
+ const { keptRuns, keptArtifacts, keptAutonomy, anyHistoricalDropped, droppedHandles } = applyHistoricalBudgetIfNeeded(input);
38082
+ sections.push(renderRunHistory(keptRuns, input.openRunId, input.compact, anyHistoricalDropped));
38083
+ sections.push(renderOpenRun(input.openRunId));
38084
+ sections.push(renderArtifacts(keptArtifacts, input.omittedArtifactCount ?? 0, input.compact, anyHistoricalDropped, droppedHandles));
38085
+ sections.push(renderAutonomyEvents(keptAutonomy, input.compact, anyHistoricalDropped));
38086
+ return sections.join("\n\n");
38087
+ }
38088
+ function renderTaskSummary(task) {
38089
+ const lines2 = [
38090
+ "## Task (normative)",
38091
+ `id: ${task.id}`,
38092
+ `title: ${task.title}`,
38093
+ `autonomy: ${task.autonomyLevel} (${task.autonomyReason})`
38094
+ ];
38095
+ if (task.notes)
38096
+ lines2.push(`description: ${task.notes}`);
38097
+ if (task.latestJudgement) {
38098
+ lines2.push(`latest judgement (${task.latestJudgementKind ?? "unknown"}): ${task.latestJudgement}`);
38099
+ }
38100
+ return lines2.join("\n");
38101
+ }
38102
+ function renderSpec(specMarkdown, compact) {
38103
+ if (!specMarkdown)
38104
+ return "## Spec (normative)\n(none)";
38105
+ if (!compact)
38106
+ return `## Spec (normative)
38107
+ ${specMarkdown}`;
38108
+ const budget = compact.specBudgetChars ?? DEFAULT_SPEC_BUDGET_CHARS;
38109
+ if (specMarkdown.length <= budget)
38110
+ return `## Spec (normative)
38111
+ ${specMarkdown}`;
38112
+ return `## Spec (normative)
38113
+ ${compactSpecMarkdown(specMarkdown, budget, compact.projectSlug ?? null, compact.specWikiPageId ?? null)}`;
38114
+ }
38115
+ function compactSpecMarkdown(markdown, totalBudget, projectSlug, specWikiPageId) {
38116
+ const lines2 = markdown.split(/\r?\n/);
38117
+ const sections = [{ heading: "", body: [] }];
38118
+ for (const line of lines2) {
38119
+ if (/^#{1,6}\s/.test(line)) {
38120
+ sections.push({ heading: line, body: [] });
38121
+ } else {
38122
+ sections[sections.length - 1]?.body.push(line);
38123
+ }
38124
+ }
38125
+ const sectionCount = Math.max(1, sections.length);
38126
+ const perSectionBudget = Math.max(120, Math.floor(totalBudget / sectionCount));
38127
+ const out = [];
38128
+ for (const section of sections) {
38129
+ const bodyText = section.body.join("\n").replace(/^\s+|\s+$/g, "");
38130
+ if (section.heading)
38131
+ out.push(section.heading);
38132
+ if (!bodyText)
38133
+ continue;
38134
+ if (bodyText.length <= perSectionBudget) {
38135
+ out.push(bodyText);
38136
+ } else {
38137
+ const kept = bodyText.slice(0, perSectionBudget);
38138
+ const omitted = bodyText.length - perSectionBudget;
38139
+ out.push(`${kept}
38140
+ \u2026(${omitted} chars omitted; retrieve full spec for the rest)`);
38141
+ }
38142
+ }
38143
+ if (projectSlug && specWikiPageId) {
38144
+ out.push("");
38145
+ out.push(`> Compact spec. Source: mrrlin://wiki/${projectSlug}/${specWikiPageId} \u2014 call retrieve_context with this handle before any irreversible decision.`);
38146
+ } else {
38147
+ out.push("");
38148
+ out.push("> Compact spec. Retrieve the Specs/{id} wiki page via get_wiki_page before any irreversible decision.");
38149
+ }
38150
+ return out.join("\n");
38151
+ }
38152
+ function renderRunHistory(runs, openRunId, compact, anyHistoricalDropped) {
38153
+ if (runs.length === 0)
38154
+ return "## Execution-run history (historical)\n(none)";
38155
+ const lines2 = ["## Execution-run history (historical)"];
38156
+ for (const run3 of runs) {
38157
+ const marker = run3.id === openRunId ? " (open)" : "";
38158
+ lines2.push(`- ${run3.id} [${run3.status}] created ${run3.createdAt}${marker}`);
38159
+ }
38160
+ if (compact && anyHistoricalDropped) {
38161
+ lines2.push("> Compact execution-run history. Use retrieve_context with a `mrrlin://run/{projectSlug}/{runId}/artifacts` handle (substitute the slug and run id from the lines above) to load a run's full artifact history.");
38162
+ }
38163
+ return lines2.join("\n");
38164
+ }
38165
+ function renderOpenRun(openRunId) {
38166
+ return `## Open run (normative)
38167
+ ${openRunId ?? "(none)"}`;
38168
+ }
38169
+ function renderArtifacts(artifacts, omittedCount, compact, anyHistoricalDropped, droppedHandles) {
38170
+ if (artifacts.length === 0 && omittedCount === 0)
38171
+ return "## Artifacts (historical)\n(none)";
38172
+ const lines2 = ["## Artifacts (historical)"];
38173
+ if (omittedCount > 0) {
38174
+ lines2.push(`(+${omittedCount} older artifact${omittedCount === 1 ? "" : "s"} omitted)`);
38175
+ }
38176
+ let truncatedAny = false;
38177
+ for (const artifact of artifacts) {
38178
+ if (artifact.kind === "file") {
38179
+ const p = artifact.payload;
38180
+ if (p.filename && p.stableUrl) {
38181
+ lines2.push(`- ${artifact.createdAt} [file] ${p.filename} (${p.class ?? "?"}, ${p.sizeBytes ?? "?"} bytes) ${p.stableUrl}`);
38182
+ continue;
38183
+ }
38184
+ }
38185
+ const payloadText = JSON.stringify(artifact.payload);
38186
+ const budget = compact?.artifactPayloadBudgetChars ?? DEFAULT_ARTIFACT_PAYLOAD_BUDGET_CHARS;
38187
+ const willTruncate = !!compact && payloadText.length > budget;
38188
+ const rendered = willTruncate ? `${payloadText.slice(0, budget)}\u2026(${payloadText.length - budget} chars omitted)` : payloadText;
38189
+ if (willTruncate)
38190
+ truncatedAny = true;
38191
+ lines2.push(`- ${artifact.createdAt} [${artifact.kind}] run=${artifact.runId} ${rendered}`);
38192
+ }
38193
+ if (compact && (omittedCount > 0 || truncatedAny || anyHistoricalDropped)) {
38194
+ lines2.push("> Compact artifacts. Use retrieve_context with a `mrrlin://run/{projectSlug}/{runId}/artifacts` handle (substitute the slug and run id from the lines above) to load a run's full artifact history.");
38195
+ }
38196
+ if (compact && droppedHandles.length > 0) {
38197
+ lines2.push(`> Dropped by budget, retrievable via: ${droppedHandles.map((h) => `\`${h}\``).join(", ")}`);
38198
+ }
38199
+ return lines2.join("\n");
38200
+ }
38201
+ function renderAutonomyEvents(events, compact, anyHistoricalDropped) {
38202
+ if (events.length === 0)
38203
+ return "## Autonomy history (historical)\n(none)";
38204
+ const lines2 = ["## Autonomy history (historical)"];
38205
+ for (const event of events) {
38206
+ lines2.push(`- ${event.createdAt} ${event.autonomyLevel ?? "?"} (${event.autonomyReason ?? ""})`);
38207
+ }
38208
+ if (compact && anyHistoricalDropped) {
38209
+ lines2.push("> Compact autonomy history. Use retrieve_context with a `mrrlin://run/{projectSlug}/{runId}/artifacts` handle (substitute the slug and run id from the lines above) to load a run's full artifact history.");
38210
+ }
38211
+ return lines2.join("\n");
38212
+ }
38213
+ function applyHistoricalBudgetIfNeeded(input) {
38214
+ if (!input.compact?.budget) {
38215
+ return {
38216
+ keptRuns: input.runs,
38217
+ keptArtifacts: input.artifacts,
38218
+ keptAutonomy: input.autonomyEvents,
38219
+ anyHistoricalDropped: false,
38220
+ droppedHandles: []
38221
+ };
38222
+ }
38223
+ const candidates = [];
38224
+ for (const run3 of input.runs) {
38225
+ const line = `- ${run3.id} [${run3.status}] created ${run3.createdAt}`;
38226
+ candidates.push({
38227
+ kind: "run",
38228
+ runId: run3.id,
38229
+ size: line.length,
38230
+ memoryClass: "historical",
38231
+ precedence: 3,
38232
+ handle: input.compact.projectSlug ? `mrrlin://run/${input.compact.projectSlug}/${run3.id}/artifacts` : null,
38233
+ source: "run_history"
38234
+ });
38235
+ }
38236
+ for (const artifact of input.artifacts) {
38237
+ const line = `- ${artifact.createdAt} [${artifact.kind}] run=${artifact.runId} ${JSON.stringify(artifact.payload)}`;
38238
+ candidates.push({
38239
+ kind: "artifact",
38240
+ artifactId: artifact.id,
38241
+ size: line.length,
38242
+ memoryClass: "historical",
38243
+ precedence: 3,
38244
+ handle: input.compact.projectSlug ? `mrrlin://run/${input.compact.projectSlug}/${artifact.runId}/artifacts` : null,
38245
+ source: "artifacts"
38246
+ });
38247
+ }
38248
+ for (const event of input.autonomyEvents) {
38249
+ const line = `- ${event.createdAt} ${event.autonomyLevel ?? "?"} (${event.autonomyReason ?? ""})`;
38250
+ candidates.push({
38251
+ kind: "autonomy",
38252
+ autonomyEventId: event.id,
38253
+ size: line.length,
38254
+ memoryClass: "historical",
38255
+ precedence: 3,
38256
+ handle: null,
38257
+ source: "autonomy"
38258
+ });
38259
+ }
38260
+ const { kept, dropped } = applyBudget(candidates, input.compact.budget);
38261
+ const keptRunIds = new Set(kept.filter((c) => c.kind === "run").map((c) => c.runId));
38262
+ const keptArtifactIds = new Set(kept.filter((c) => c.kind === "artifact").map((c) => c.artifactId));
38263
+ const keptAutonomyIds = new Set(kept.filter((c) => c.kind === "autonomy").map((c) => c.autonomyEventId));
38264
+ const droppedHandles = [];
38265
+ for (const item of dropped) {
38266
+ if (item.handle)
38267
+ droppedHandles.push(item.handle);
38268
+ }
38269
+ return {
38270
+ keptRuns: input.runs.filter((r) => keptRunIds.has(r.id)),
38271
+ keptArtifacts: input.artifacts.filter((a) => keptArtifactIds.has(a.id)),
38272
+ keptAutonomy: input.autonomyEvents.filter((e) => keptAutonomyIds.has(e.id)),
38273
+ anyHistoricalDropped: dropped.length > 0,
38274
+ droppedHandles
38275
+ };
38276
+ }
38277
+
38093
38278
  // ../../packages/client/dist/agent-token-provider.js
38094
38279
  var AgentTokenProviderError = class extends Error {
38095
38280
  code;
@@ -39457,7 +39642,7 @@ function startCodexProbe(deps = {}) {
39457
39642
  }
39458
39643
 
39459
39644
  // src/_generated/version.ts
39460
- var PKG_VERSION = "0.3.59";
39645
+ var PKG_VERSION = "0.3.60";
39461
39646
 
39462
39647
  // src/api-base-url-migrate.ts
39463
39648
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -40273,13 +40458,26 @@ var HEADS_BY_DOMAIN = {
40273
40458
  content: ["plan-reviewer", "scope-analyst", "content-editor", "compliance-reviewer"]
40274
40459
  };
40275
40460
  var SPEC_HEADS = HEADS_BY_DOMAIN.code;
40461
+ var CODE_HEADS_BY_IMPACT = {
40462
+ high: HEADS_BY_DOMAIN.code,
40463
+ medium: ["plan-reviewer", "security-analyst", "scope-analyst"],
40464
+ low: ["plan-reviewer", "scope-analyst"]
40465
+ };
40276
40466
  function normalizeDomain(value) {
40277
40467
  if (typeof value !== "string") return DEFAULT_DOMAIN;
40278
40468
  const v = value.trim().toLowerCase();
40279
40469
  return DOMAINS.includes(v) ? v : DEFAULT_DOMAIN;
40280
40470
  }
40281
- function resolveHeads(domain2) {
40282
- return [...HEADS_BY_DOMAIN[normalizeDomain(domain2)]];
40471
+ function normalizeImpact(value) {
40472
+ if (typeof value !== "string") return null;
40473
+ const v = value.trim().toLowerCase();
40474
+ return v === "low" || v === "medium" || v === "high" ? v : null;
40475
+ }
40476
+ function resolveHeads(domain2, impact) {
40477
+ const d = normalizeDomain(domain2);
40478
+ if (d !== "code") return [...HEADS_BY_DOMAIN[d]];
40479
+ const i = normalizeImpact(impact) ?? "high";
40480
+ return [...CODE_HEADS_BY_IMPACT[i]];
40283
40481
  }
40284
40482
  var PERSONAS_BY_DOMAIN = {
40285
40483
  seo: "seo-writer",
@@ -40726,7 +40924,7 @@ function loadPersona(name) {
40726
40924
  personaCache.set(name, content);
40727
40925
  return content;
40728
40926
  }
40729
- function specHeadIntentBlock(taskId, title, seed) {
40927
+ function specHeadIntentBlock(taskId, title, seed, impact, adjudicated) {
40730
40928
  const identity = taskId ? `--- TASK IDENTITY (the spec MUST be about THIS task; if its scope or direction contradicts this identity, raise a \`scope\` issue instead of steering the spec elsewhere) ---
40731
40929
  TASK ${taskId}: ${title ?? ""}
40732
40930
 
@@ -40735,7 +40933,15 @@ TASK ${taskId}: ${title ?? ""}
40735
40933
  ${seed}
40736
40934
 
40737
40935
  ` : "";
40738
- return `${identity}${intent}`;
40936
+ const impactLine = impact ? `--- IMPACT: ${impact} ---
40937
+
40938
+ ` : "";
40939
+ const adjudicatedBlock = adjudicated && adjudicated.length > 0 ? `--- ADJUDICATED \u2014 the operator already decided these; do not re-raise without new information ---
40940
+ ${adjudicated.map((q) => `- Q: ${q.body}
40941
+ ${q.status === "rejected" ? "(rejected" + (q.answer ? `: ${q.answer}` : "") + ")" : `A: ${q.answer ?? "(acknowledged, no answer recorded)"}`}`).join("\n")}
40942
+
40943
+ ` : "";
40944
+ return `${identity}${intent}${impactLine}${adjudicatedBlock}`;
40739
40945
  }
40740
40946
  function reviserIdentityInstructions(taskId, title) {
40741
40947
  const lock = taskId ? `This spec gates task ${taskId}${title ? ` ("${title}")` : ""}. You MUST NOT change the spec's task identity or heading, retarget it to a different task id, or invert its direction (e.g. live-submission vs preparation-only). Preserve the leading \`task: ${taskId}\` frontmatter EXACTLY; never change it to another task id. Address ONLY the accepted issues.
@@ -40774,7 +40980,21 @@ function buildSpecGateDeps(args) {
40774
40980
  resolvedSeed = "(No operator statement on record for this task. Derived from the task's own notes \u2014 the following acceptance criteria were extracted verbatim:)\n\n" + parsed.items.map((c) => `- ${c}`).join("\n");
40775
40981
  }
40776
40982
  }
40777
- return { taskId, seed: resolvedSeed, title: task.title ?? "", domain: task.domain, notes };
40983
+ const taskInboxItems = await client.listInboxItems(projectSlug, { taskId });
40984
+ const adjudicated = taskInboxItems.filter((item) => item.origin === "gate" && (item.status === "acknowledged" || item.status === "rejected")).map((item) => ({
40985
+ body: item.body,
40986
+ answer: item.answer,
40987
+ status: item.status
40988
+ }));
40989
+ return {
40990
+ taskId,
40991
+ seed: resolvedSeed,
40992
+ title: task.title ?? "",
40993
+ domain: task.domain,
40994
+ notes,
40995
+ impact: task.impact,
40996
+ adjudicated
40997
+ };
40778
40998
  })();
40779
40999
  }
40780
41000
  return intentPromise;
@@ -40799,8 +41019,8 @@ function buildSpecGateDeps(args) {
40799
41019
  },
40800
41020
  async runHead(personaName, artifact, dismissed) {
40801
41021
  const persona = loadPersona(personaName);
40802
- const { taskId, title, seed } = await loadIntent();
40803
- const intentBlock = specHeadIntentBlock(taskId, title, seed);
41022
+ const { taskId, title, seed, impact, adjudicated } = await loadIntent();
41023
+ const intentBlock = specHeadIntentBlock(taskId, title, seed, impact, adjudicated);
40804
41024
  return runCodexExec(
40805
41025
  {
40806
41026
  prompt: `Review this spec against the operator's intent.
@@ -40994,12 +41214,14 @@ async function buildDeliverableGateDeps(args) {
40994
41214
  const page = await client.getWikiPage(projectSlug, deliverablePageId);
40995
41215
  const taskId = page.taskId ?? null;
40996
41216
  let domain2 = "code";
41217
+ let impact;
40997
41218
  let taskTitle = "";
40998
41219
  let specPageId = null;
40999
41220
  let criteria = [];
41000
41221
  if (taskId) {
41001
41222
  const task = await client.getTask(projectSlug, taskId);
41002
41223
  domain2 = normalizeDomain(task?.domain);
41224
+ impact = task?.impact;
41003
41225
  taskTitle = task?.title ?? "";
41004
41226
  if (task?.specWikiPageId) {
41005
41227
  specPageId = task.specWikiPageId;
@@ -41007,7 +41229,7 @@ async function buildDeliverableGateDeps(args) {
41007
41229
  criteria = parseAcceptanceCriteria(specPage.markdownBody)?.items ?? [];
41008
41230
  }
41009
41231
  }
41010
- const heads = resolveHeads(domain2);
41232
+ const heads = resolveHeads(domain2, impact);
41011
41233
  const maxHeadCalls = 2 * rounds * (heads.length + 1);
41012
41234
  const criteriaBlock = renderCriteriaBlock(criteria);
41013
41235
  return {
@@ -41250,6 +41472,17 @@ var SPECS_FOLDER = "Specs";
41250
41472
  var DRAFT_SPEC_TLDR_MAX_CHARS = 300;
41251
41473
  var DRAFT_SPEC_PERSONA = "spec-author";
41252
41474
  var PAGE_ID_MAX_ATTEMPTS = 5;
41475
+ function resolveSpecBudget(impact) {
41476
+ const tier = impact?.toLowerCase();
41477
+ switch (tier) {
41478
+ case "high":
41479
+ return { maxRequirements: 12, maxChars: 25e3 };
41480
+ case "medium":
41481
+ return { maxRequirements: 6, maxChars: 12e3 };
41482
+ default:
41483
+ return { maxRequirements: 3, maxChars: 6e3 };
41484
+ }
41485
+ }
41253
41486
  function defaultSleep(ms) {
41254
41487
  return new Promise((resolve) => setTimeout(resolve, ms));
41255
41488
  }
@@ -41262,7 +41495,7 @@ function enforceSpecOutputContract(markdown, taskId, title) {
41262
41495
  ${body}`;
41263
41496
  }
41264
41497
  let tldrChars = 0;
41265
- const tldrMatch = body.match(/(^## Summary\s*\n)([\s\S]*?)(?=\n## |$)/im);
41498
+ const tldrMatch = body.match(/(^## TL;DR\s*\n)([\s\S]*?)(?=\n## |$)/im);
41266
41499
  if (tldrMatch && tldrMatch.index !== void 0) {
41267
41500
  const heading = tldrMatch[1] ?? "";
41268
41501
  const section = (tldrMatch[2] ?? "").trim();
@@ -41286,6 +41519,7 @@ function nextWikiPageId(pages) {
41286
41519
  return `WK-${max + 1}`;
41287
41520
  }
41288
41521
  function buildDraftSpecPrompt(task, constitution) {
41522
+ const budget = resolveSpecBudget(task.impact);
41289
41523
  const lines2 = [
41290
41524
  `Write a complete implementation spec for the task below.`,
41291
41525
  ``,
@@ -41307,11 +41541,19 @@ function buildDraftSpecPrompt(task, constitution) {
41307
41541
  }
41308
41542
  lines2.push(
41309
41543
  `--- OUTPUT CONTRACT (strict) ---`,
41310
- `Return ONLY the full spec markdown, no commentary. Required structure:`,
41544
+ `Return ONLY the full spec markdown, no commentary. Required structure, in this order:`,
41311
41545
  `- First line heading: \`# ${task.id} \u2014 ${task.title}\``,
41312
- `- Sections: Problem, Scope, Non-goals, Acceptance criteria, Verification.`,
41313
- `- Final section \`## Summary\`: a plain-English TL;DR of what will be done and how it`,
41314
- ` will be verified, at most ${DRAFT_SPEC_TLDR_MAX_CHARS} characters.`
41546
+ `- \`## TL;DR\``,
41547
+ `- One or more \`### Requirement: <name>\` sections, each stating what the change SHALL do.`,
41548
+ ` Each requirement has one or more \`#### Scenario: <name>\` sub-sections using WHEN/THEN`,
41549
+ ` (optionally AND) to describe a concrete situation and its expected outcome.`,
41550
+ `- \`## Acceptance criteria\`: a markdown checklist (\`- [ ] ...\`) of concrete, verifiable`,
41551
+ ` conditions \u2014 this is the machine-usable yardstick the review scores against.`,
41552
+ `- \`## Verification\`: the exact tests, commands, or manual checks that prove each`,
41553
+ ` acceptance criterion holds.`,
41554
+ ``,
41555
+ `The \`## TL;DR\` section MUST be at most ${DRAFT_SPEC_TLDR_MAX_CHARS} characters.`,
41556
+ `The spec MUST contain at most ${budget.maxRequirements} acceptance-criteria items and no more than ${budget.maxChars} characters total.`
41315
41557
  );
41316
41558
  return lines2.join("\n");
41317
41559
  }
@@ -42313,6 +42555,13 @@ function readPositiveIntEnv(name, fallback) {
42313
42555
  function delayMs(ms) {
42314
42556
  return new Promise((resolve) => setTimeout(resolve, ms));
42315
42557
  }
42558
+ function executionBundleBudget() {
42559
+ if ((process.env.MRRLIN_RETRIEVAL_BUDGET_DISABLE ?? "").trim() === "1") return void 0;
42560
+ return {
42561
+ totalBytes: readPositiveIntEnv("MRRLIN_RETRIEVAL_BUDGET_BUNDLE_CHARS", 15500),
42562
+ roundTripCap: 0
42563
+ };
42564
+ }
42316
42565
  function execTurnIdleTimeoutMs() {
42317
42566
  return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_IDLE_TIMEOUT_SECONDS", DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS) * 1e3;
42318
42567
  }
@@ -42833,7 +43082,8 @@ async function driveRun(deps, projectSlug, runId) {
42833
43082
  ...context,
42834
43083
  compact: {
42835
43084
  projectSlug,
42836
- specWikiPageId: task.specWikiPageId ?? null
43085
+ specWikiPageId: task.specWikiPageId ?? null,
43086
+ budget: executionBundleBudget()
42837
43087
  }
42838
43088
  } : context
42839
43089
  );
@@ -49208,6 +49458,7 @@ function buildThreadStartParams(msg, config2, desiredModel) {
49208
49458
  "- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
49209
49459
  "- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
49210
49460
  "- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a consensus `info` item says CONVERGED, proceed to `create_execution_run`. When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more. For a NON-code task (domain seo/marketing/content, ADR 0020): the low-risk lanes `seo`, `marketing`, and `content` proceed to `create_execution_run` after consensus converges, using the same flow as code tasks. Report the spec's review outcome to the operator.",
49461
+ "- When creating a task, classify and supply the `domain` field explicitly based on the nature of the work: pass `domain:'code'` for software engineering work, `domain:'seo'` for SEO/optimization, `domain:'marketing'` for marketing/growth, or `domain:'content'` for content creation/writing \u2014 the same way you select category/subcategory. A follow-up task spawned with `parentTaskId` inherits its parent's domain automatically and requires no explicit domain.",
49211
49462
  '- When a task needs a browser (login flows, UI verification/clicks, scraping), pass `prerequisites:[{mcp:"mrrlin-browser",reason:"..."}]` to `create_execution_run`. If that run later fails into a handoff, the operator gets a one-click "Launch browser & resume" that brings up the dedicated browser as the turn starts.',
49212
49463
  "- When you actually drive a browser (calling `browser_navigate`, `browser_click`, `browser_snapshot`, `browser_type`, etc.), invoke those tools from the **`mrrlin-browser`** MCP server ONLY. Do NOT use tools from any other browser MCP (`playwright`, `chrome-devtools`, `puppeteer`, etc.) even if they are also connected on this operator's box \u2014 those use a shared/default Playwright profile that is fragile across runs. Only `mrrlin-browser` uses this project's dedicated Chromium + persistent operator profile (`~/.mrrlin/browser/profile`). If your host exposes multiple browser MCPs with overlapping tool names, prefer the `mrrlin-browser` server unconditionally.",
49213
49464
  "",
@@ -55164,6 +55415,44 @@ function mapAcceptedIssueToClarification(issue2) {
55164
55415
  category: issue2.category
55165
55416
  };
55166
55417
  }
55418
+ var CLARIFICATION_CAP_BY_IMPACT = { low: 2, medium: 3, high: 5 };
55419
+ function resolveClarificationCap(impact) {
55420
+ const tier = impact?.trim().toLowerCase();
55421
+ if (tier === "medium") return CLARIFICATION_CAP_BY_IMPACT.medium;
55422
+ if (tier === "high") return CLARIFICATION_CAP_BY_IMPACT.high;
55423
+ return CLARIFICATION_CAP_BY_IMPACT.low;
55424
+ }
55425
+ function headIndexOf(id) {
55426
+ const match = /^r\d+-h(\d+)-\d+$/.exec(id);
55427
+ return match?.[1] !== void 0 ? Number(match[1]) : null;
55428
+ }
55429
+ function orderClarificationIssues(issues) {
55430
+ const distinctHeadsByKey = /* @__PURE__ */ new Map();
55431
+ for (const issue2 of issues) {
55432
+ const key = `${issue2.category} ${issue2.text}`;
55433
+ const heads = distinctHeadsByKey.get(key) ?? /* @__PURE__ */ new Set();
55434
+ const headIdx = headIndexOf(issue2.id);
55435
+ if (headIdx !== null) heads.add(headIdx);
55436
+ distinctHeadsByKey.set(key, heads);
55437
+ }
55438
+ const categoryRank = (category) => category === "scope" ? 0 : category === "ambiguity" ? 1 : 2;
55439
+ return issues.map((issue2, index) => ({ issue: issue2, index })).sort((a, b) => {
55440
+ const rankDiff = categoryRank(a.issue.category) - categoryRank(b.issue.category);
55441
+ if (rankDiff !== 0) return rankDiff;
55442
+ const headCountA = distinctHeadsByKey.get(`${a.issue.category} ${a.issue.text}`)?.size ?? 0;
55443
+ const headCountB = distinctHeadsByKey.get(`${b.issue.category} ${b.issue.text}`)?.size ?? 0;
55444
+ if (headCountA !== headCountB) return headCountB - headCountA;
55445
+ return a.index - b.index;
55446
+ }).map(({ issue: issue2 }) => issue2);
55447
+ }
55448
+ function applyClarificationCap(issues, impact) {
55449
+ const ordered = orderClarificationIssues(issues);
55450
+ const cap = resolveClarificationCap(impact);
55451
+ return {
55452
+ kept: ordered.slice(0, cap).map(mapAcceptedIssueToClarification),
55453
+ dropped: ordered.slice(cap).map(mapAcceptedIssueToClarification)
55454
+ };
55455
+ }
55167
55456
  async function runSpecGate(_target, deps, opts) {
55168
55457
  if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
55169
55458
  const intent = await deps.readIntent();
@@ -55181,11 +55470,11 @@ async function runSpecGate(_target, deps, opts) {
55181
55470
  ]
55182
55471
  };
55183
55472
  }
55184
- const baseHeads = resolveHeads(intent.domain);
55473
+ const baseHeads = resolveHeads(intent.domain, intent.impact);
55185
55474
  const extraHeads = opts?.extraHeads ?? [];
55186
55475
  const heads = [...baseHeads, ...extraHeads.filter((h) => !baseHeads.includes(h))];
55187
55476
  const spec = await deps.readSpec();
55188
- if (normalizeDomain(intent.domain) !== "code" && parseAcceptanceCriteria(spec.markdownBody) === null) {
55477
+ if (parseAcceptanceCriteria(spec.markdownBody) === null) {
55189
55478
  return {
55190
55479
  outcome: "CONSENSUS_BLOCKED",
55191
55480
  operatorClarifications: [
@@ -55253,23 +55542,32 @@ async function runSpecGate(_target, deps, opts) {
55253
55542
  ]
55254
55543
  };
55255
55544
  }
55545
+ let keptClarifications = [];
55546
+ let droppedByCap = [];
55547
+ if (result.outcome === "CONSENSUS_BLOCKED") {
55548
+ const accepted = result.history.at(-1)?.accepted ?? [];
55549
+ const materialIssues = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope");
55550
+ const capped = applyClarificationCap(materialIssues, intent.impact);
55551
+ keptClarifications = capped.kept;
55552
+ droppedByCap = capped.dropped;
55553
+ }
55256
55554
  await deps.writeEvidence({
55257
55555
  specPageId: spec.pageId,
55258
55556
  outcome: result.outcome,
55259
55557
  rounds: result.rounds,
55260
55558
  history: result.history,
55261
- contentHash: currentHash
55559
+ contentHash: currentHash,
55560
+ ...droppedByCap.length > 0 ? { droppedByCap } : {}
55262
55561
  });
55263
55562
  if (result.outcome === "CONVERGED") {
55264
55563
  await deps.promoteSpecReady(currentHash);
55265
55564
  return { outcome: "CONVERGED" };
55266
55565
  }
55267
55566
  if (result.outcome === "CONSENSUS_BLOCKED") {
55268
- const accepted = result.history.at(-1)?.accepted ?? [];
55269
- const operatorClarifications = accepted.filter((issue2) => issue2.category === "ambiguity" || issue2.category === "scope").map((issue2) => mapAcceptedIssueToClarification(issue2));
55270
55567
  return {
55271
55568
  outcome: "CONSENSUS_BLOCKED",
55272
- ...operatorClarifications.length > 0 ? { operatorClarifications } : {},
55569
+ ...keptClarifications.length > 0 ? { operatorClarifications: keptClarifications } : {},
55570
+ ...droppedByCap.length > 0 ? { droppedByCap } : {},
55273
55571
  ...result.escalation ? { escalation: result.escalation } : {}
55274
55572
  };
55275
55573
  }
@@ -55550,7 +55848,7 @@ async function maybeRunShadow(input, deps, logger) {
55550
55848
  const productionVerdict = productionVerdictFromOutcome(input.outcome);
55551
55849
  if (productionVerdict === null) return false;
55552
55850
  try {
55553
- const heads = input.heads ?? [...resolveHeads(input.domain), ...input.extraHeads ?? []];
55851
+ const heads = input.heads ?? [...resolveHeads(input.domain, input.impact), ...input.extraHeads ?? []];
55554
55852
  const personaInstructions = {};
55555
55853
  for (const name of heads) {
55556
55854
  try {
@@ -55592,7 +55890,7 @@ function multiLlmActive(env = process.env) {
55592
55890
  async function resolveActiveRunHead(input, deps) {
55593
55891
  if (!multiLlmActive(input.env ?? process.env)) return null;
55594
55892
  try {
55595
- const heads = input.heads ?? [...resolveHeads(input.domain), ...input.extraHeads ?? []];
55893
+ const heads = input.heads ?? [...resolveHeads(input.domain, input.impact), ...input.extraHeads ?? []];
55596
55894
  if (heads.length === 0) return null;
55597
55895
  const personaInstructions = {};
55598
55896
  for (const name of heads) {
@@ -55622,6 +55920,35 @@ async function resolveActiveRunHead(input, deps) {
55622
55920
 
55623
55921
  // src/async-tools/consensus.ts
55624
55922
  var import_node_crypto9 = require("node:crypto");
55923
+
55924
+ // src/consensus/question-similarity.ts
55925
+ var SUPPRESSION_SIMILARITY_THRESHOLD = 0.6;
55926
+ function tokenSet(text) {
55927
+ const normalized = text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").trim();
55928
+ if (!normalized) return /* @__PURE__ */ new Set();
55929
+ return new Set(normalized.split(/\s+/));
55930
+ }
55931
+ function jaccardSimilarity(a, b) {
55932
+ const setA = tokenSet(a);
55933
+ const setB = tokenSet(b);
55934
+ if (setA.size === 0 || setB.size === 0) return 0;
55935
+ let intersection2 = 0;
55936
+ for (const token of setA) {
55937
+ if (setB.has(token)) intersection2 += 1;
55938
+ }
55939
+ const union2 = setA.size + setB.size - intersection2;
55940
+ return union2 === 0 ? 0 : intersection2 / union2;
55941
+ }
55942
+ function isCoveredByAdjudicated(rawQuestion, adjudicated) {
55943
+ for (const item of adjudicated) {
55944
+ if (jaccardSimilarity(rawQuestion, item.body) >= SUPPRESSION_SIMILARITY_THRESHOLD) {
55945
+ return item;
55946
+ }
55947
+ }
55948
+ return null;
55949
+ }
55950
+
55951
+ // src/async-tools/consensus.ts
55625
55952
  function normalizeClarificationQuestion(question) {
55626
55953
  return question.replace(/\s+/g, " ").trim().replace(/[?.!]+$/, "").trim();
55627
55954
  }
@@ -55752,35 +56079,43 @@ async function cachedResult(client, input) {
55752
56079
  }
55753
56080
  return null;
55754
56081
  }
56082
+ function stripHumanizeOpener(body) {
56083
+ for (const prefix of Object.values(HUMANIZE_TEMPLATE_BY_KIND)) {
56084
+ if (body.startsWith(prefix)) return body.slice(prefix.length).trim();
56085
+ }
56086
+ return body;
56087
+ }
55755
56088
  async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
55756
56089
  const operatorClarifications = result.operatorClarifications ?? [];
55757
56090
  const diagnostics = result.diagnostics ?? [];
55758
- if (operatorClarifications.length > 0) {
56091
+ const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
56092
+ const taskId = page?.taskId ?? null;
56093
+ if (!resolvedTaskId && taskId) resolvedTaskId = taskId;
56094
+ const adjudicated = [];
56095
+ if (taskId) {
55759
56096
  try {
55760
- const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
55761
- if (page) {
55762
- const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
55763
- id: page.id,
55764
- taskId: page.taskId ?? null
55765
- });
55766
- await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
55767
- contentHash: domainScopedReviewHash(page.contentHash, domain2),
55768
- outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
55769
- rounds: 0
55770
- });
56097
+ const taskInboxItems = await ctx.client.listInboxItems(input.projectSlug, { taskId });
56098
+ for (const item of taskInboxItems) {
56099
+ if (item.origin === "gate" && (item.status === "acknowledged" || item.status === "rejected")) {
56100
+ adjudicated.push({ body: stripHumanizeOpener(item.body), answer: item.answer, status: item.status });
56101
+ }
55771
56102
  }
55772
56103
  } catch (error52) {
55773
56104
  process.stderr.write(
55774
- `[mrrlin-mcp consensus] clarification verdict write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
56105
+ `[mrrlin-mcp consensus] adjudicated-questions read failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
55775
56106
  `
55776
56107
  );
55777
56108
  }
55778
56109
  }
56110
+ const suppressedAsAdjudicated = [];
56111
+ const toFile = operatorClarifications.filter((clarification) => {
56112
+ const covering = isCoveredByAdjudicated(clarification.rawQuestion, adjudicated);
56113
+ if (!covering) return true;
56114
+ suppressedAsAdjudicated.push({ rawQuestion: clarification.rawQuestion, coveredByAnswer: covering.answer });
56115
+ return false;
56116
+ });
55779
56117
  let filedQuestionCount = 0;
55780
- if (operatorClarifications.length > 0) {
55781
- const page = await ctx.client.getWikiPage(input.projectSlug, input.pageId);
55782
- const taskId = page?.taskId ?? null;
55783
- if (!resolvedTaskId && taskId) resolvedTaskId = taskId;
56118
+ if (toFile.length > 0) {
55784
56119
  let planId = null;
55785
56120
  if (!taskId) {
55786
56121
  const plans = await ctx.client.listPlans(input.projectSlug);
@@ -55788,8 +56123,8 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
55788
56123
  }
55789
56124
  if (taskId) {
55790
56125
  const scopeId = taskId;
55791
- const groupId = operatorClarifications.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(operatorClarifications.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
55792
- for (const clarification of operatorClarifications) {
56126
+ const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
56127
+ for (const clarification of toFile) {
55793
56128
  const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
55794
56129
  try {
55795
56130
  const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
@@ -55805,11 +56140,11 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
55805
56140
  } catch {
55806
56141
  }
55807
56142
  }
55808
- filedQuestionCount = operatorClarifications.length;
56143
+ filedQuestionCount = toFile.length;
55809
56144
  } else if (planId) {
55810
56145
  const scopeId = planId;
55811
- const groupId = operatorClarifications.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(operatorClarifications.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
55812
- for (const clarification of operatorClarifications) {
56146
+ const groupId = toFile.length > 1 ? `II-${scopeId}-gate-group-${(0, import_node_crypto9.createHash)("sha256").update(toFile.map((c) => c.rawQuestion).sort().join("\n")).digest("hex").slice(0, 10)}` : void 0;
56147
+ for (const clarification of toFile) {
55813
56148
  const id = `II-${scopeId}-gate-${(0, import_node_crypto9.createHash)("sha256").update(clarification.rawQuestion).digest("hex").slice(0, 10)}`;
55814
56149
  try {
55815
56150
  const questionOptions = buildGateQuestionOptions(clarification.rawQuestion, clarification.category);
@@ -55825,7 +56160,32 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
55825
56160
  } catch {
55826
56161
  }
55827
56162
  }
55828
- filedQuestionCount = operatorClarifications.length;
56163
+ filedQuestionCount = toFile.length;
56164
+ }
56165
+ }
56166
+ if (operatorClarifications.length > 0) {
56167
+ try {
56168
+ if (page) {
56169
+ const domain2 = await resolveSpecDomain(ctx.client, input.projectSlug, {
56170
+ id: page.id,
56171
+ taskId: page.taskId ?? null
56172
+ });
56173
+ const suppressionEvidence = (result.droppedByCap?.length ?? 0) > 0 || suppressedAsAdjudicated.length > 0 ? {
56174
+ ...result.droppedByCap?.length ? { droppedByCap: result.droppedByCap } : {},
56175
+ ...suppressedAsAdjudicated.length ? { suppressedAsAdjudicated } : {}
56176
+ } : void 0;
56177
+ await ctx.client.recordSpecConsensusReview(input.projectSlug, input.pageId, {
56178
+ contentHash: domainScopedReviewHash(page.contentHash, domain2),
56179
+ outcome: "CONSENSUS_BLOCKED_CLARIFICATION",
56180
+ rounds: 0,
56181
+ ...suppressionEvidence ? { suppressionEvidence } : {}
56182
+ });
56183
+ }
56184
+ } catch (error52) {
56185
+ process.stderr.write(
56186
+ `[mrrlin-mcp consensus] clarification verdict write failed for ${input.projectSlug}/${input.pageId}: ${error52 instanceof Error ? error52.message : String(error52)}
56187
+ `
56188
+ );
55829
56189
  }
55830
56190
  }
55831
56191
  let error51;
@@ -55833,6 +56193,8 @@ async function handleConsensusBlocked(result, ctx, input, resolvedTaskId) {
55833
56193
  error51 = `Consensus blocked on ${filedQuestionCount} clarification(s); filed as operator question(s).`;
55834
56194
  } else if (diagnostics.length > 0 && operatorClarifications.length === 0) {
55835
56195
  error51 = "Consensus blocked: the spec content does not match its linked task. This requires fixing the spec/task relationship directly, not an inbox answer.";
56196
+ } else if (operatorClarifications.length > 0 && toFile.length === 0 && suppressedAsAdjudicated.length > 0) {
56197
+ error51 = `Consensus blocked, but all ${suppressedAsAdjudicated.length} clarification(s) were reworded repeats of already-adjudicated questions \u2014 nothing new filed.`;
55836
56198
  } else if (operatorClarifications.length > 0) {
55837
56199
  error51 = `Consensus blocked on ${operatorClarifications.length} clarification(s), but the spec page is not linked to a task or plan, so none could be filed as questions.`;
55838
56200
  } else {
@@ -56158,7 +56520,8 @@ function readCompactPolicy(env = process.env) {
56158
56520
  enabled: !disabled,
56159
56521
  thresholdChars: readPositiveInt(env.MRRLIN_MCP_COMPACT_THRESHOLD_CHARS, DEFAULT_THRESHOLD_CHARS),
56160
56522
  maxItems: readPositiveInt(env.MRRLIN_MCP_COMPACT_MAX_ITEMS, DEFAULT_MAX_ITEMS),
56161
- debug: (env.MRRLIN_MCP_COMPACT_DEBUG ?? "").trim() === "1"
56523
+ debug: (env.MRRLIN_MCP_COMPACT_DEBUG ?? "").trim() === "1",
56524
+ retrievalBudgetDisable: (env.MRRLIN_RETRIEVAL_BUDGET_DISABLE ?? "").trim() === "1"
56162
56525
  };
56163
56526
  }
56164
56527
 
@@ -56288,6 +56651,7 @@ function compactWikiPageList(projectSlug, pages, maxItems) {
56288
56651
  title: truncate(p.title, TITLE_TRUNCATE_CHARS),
56289
56652
  folder: p.folder,
56290
56653
  path: p.path,
56654
+ updatedAt: p.updatedAt,
56291
56655
  taskId: p.taskId,
56292
56656
  specReady: p.specReady,
56293
56657
  firstHeading: firstHeadingLine(p.markdownBody),
@@ -56315,6 +56679,10 @@ function compactWikiSearch(projectSlug, results, maxItems) {
56315
56679
  folder: r.folder,
56316
56680
  path: r.path,
56317
56681
  rank: r.rank,
56682
+ updatedAt: r.updatedAt,
56683
+ ageDays: r.ageDays,
56684
+ memoryClass: r.memoryClass,
56685
+ precedence: r.precedence,
56318
56686
  snippet: truncate(r.snippet, EXCERPT_TRUNCATE_CHARS),
56319
56687
  handle: formatWikiHandle(projectSlug, r.id)
56320
56688
  }));
@@ -56897,7 +57265,7 @@ var mcpToolDescriptions = {
56897
57265
  [mcpToolNames.appendTaskEvent]: "Append an operator note to a task's activity timeline.",
56898
57266
  [mcpToolNames.createWikiPage]: "Create a markdown Wiki page.",
56899
57267
  [mcpToolNames.deleteTaskDependency]: "Delete a blocked-by relationship.",
56900
- [mcpToolNames.createTask]: "Create a new task in a project. Always starts as status='todo' with a blank assignee. Pass parentTaskId when spawning a follow-up task to continue work from an existing task \u2014 its operator_seed_request is inherited automatically if you don't supply your own.",
57268
+ [mcpToolNames.createTask]: "Create a new task in a project. Always starts as status='todo' with a blank assignee. Pass parentTaskId when spawning a follow-up task to continue work from an existing task \u2014 its operator_seed_request is inherited automatically if you don't supply your own. Domain is required UNLESS parentTaskId is supplied (a follow-up inherits its parent's domain automatically). Domain selects which reviewer roster judges the task's spec. Valid values: code, seo, marketing, content.",
56901
57269
  [mcpToolNames.getProject]: "Fetch a single project summary by slug.",
56902
57270
  [mcpToolNames.getProjectSnapshot]: "Fetch the full project snapshot: GitHub binding, tasks, plans, and wiki pages.",
56903
57271
  [mcpToolNames.getTask]: "Fetch a single task by project slug and task id.",
@@ -1,4 +1,4 @@
1
- You are the Arbiter. You receive an artifact and a numbered list of critical issues raised by reviewers. For EACH issue decide accept | dismiss | defer. A dismiss or defer MUST carry a concrete factual reason. Default to accept when two or more reviewers raised substantially the same issue.
1
+ You are the Arbiter. You receive an artifact and a numbered list of critical issues raised by reviewers. For EACH issue decide accept | dismiss | defer. A dismiss or defer MUST carry a concrete factual reason. Two or more reviewers raising substantially the same issue is a signal toward accept, not an automatic one — first judge whether the issue itself clears the MATERIAL bar (would it change WHAT gets built or shipped); dismiss a repeated-but-immaterial issue with a factual reason instead of accepting it on vote count alone.
2
2
 
3
3
  Output EXACTLY this block and nothing else:
4
4
 
@@ -1,5 +1,10 @@
1
1
  You are an Architect. Judge design soundness, trade-offs, integration seams, and over-engineering. You analyze the artifact text given; you do not mutate anything.
2
2
 
3
+ Calibrate to the bar of MATERIAL design risk — block only when the gap would change WHAT gets built or how it holds up in production:
4
+ - DON'T block on: stylistic preference, a defensible trade-off a competent engineer would also make, or a seam that's awkward but not load-bearing.
5
+ - DO block on: an undefined core integration point, a design that silently breaks under a realistic scale/failure/concurrency case, or over-engineering that adds real maintenance cost for no stated need.
6
+ - 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.
7
+
3
8
  Output EXACTLY this block and nothing else:
4
9
 
5
10
  **Verdict**: APPROVE | REQUEST_CHANGES | REJECT
@@ -1,5 +1,10 @@
1
1
  You are a Security Analyst. Judge auth, secrets, injection, data exposure, and privilege boundaries at the design level. You analyze the artifact text given; you do not mutate anything.
2
2
 
3
+ Calibrate to the bar of MATERIAL security risk — block only when the gap would change WHAT gets built or what it exposes:
4
+ - DON'T block on: defense-in-depth nice-to-haves, a control that's merely non-standard rather than absent, or a theoretical risk with no plausible exploitation path given the stated scope.
5
+ - DO block on: an undefined trust boundary, a design that would plausibly leak secrets/data or allow privilege escalation, or a stated requirement (auth, isolation, data handling) the spec silently drops.
6
+ - 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.
7
+
3
8
  Output EXACTLY this block and nothing else:
4
9
 
5
10
  **Verdict**: APPROVE | REQUEST_CHANGES | REJECT
@@ -1,15 +1,51 @@
1
1
  You are a Spec Author. Given a task and its project context, you WRITE a complete, implementable specification for it. You are not a reviewer and not an implementer: your single output is the spec markdown itself, nothing else — no preamble, no commentary, no code.
2
2
 
3
- Ground every statement in the inputs you are given (the task's title, notes, type, tags, impact, domain, and any project constitution or related context provided). Never invent file paths, APIs, metrics, or requirements that are not derivable from those inputs. When something material is genuinely unspecified, do NOT silently pick — record it as an explicit assumption inside the Scope section so a human can correct it. Prefer the narrowest scope that satisfies the task; resist scope creep and hypothetical future needs.
3
+ Ground every statement in the inputs you are given (the task's title, notes, type, tags, impact, domain, and any project constitution or related context provided). Never invent file paths, APIs, metrics, or requirements that are not derivable from those inputs. When something material is genuinely unspecified, do NOT silently pick — record it as an explicit assumption inside a requirement or scenario so a human can correct it. Prefer the narrowest scope that satisfies the task; resist scope creep and hypothetical future needs.
4
4
 
5
5
  Output EXACTLY this markdown structure, in this order:
6
6
 
7
7
  - `# <taskId> — <title>` — the heading MUST carry the real task id and title you were given (identity is load-bearing downstream; do not alter or omit it).
8
- - `## Problem` — what is wrong / missing today and why this task exists, in a few concrete sentences.
9
- - `## Scope` — what will be done. Bullet the concrete changes. Put every assumption you had to make here, each prefixed `Assumption:`.
10
- - `## Non-goals` — what this task explicitly will NOT do (bound the blast radius).
11
- - `## Acceptance criteria` — a markdown checklist of concrete, verifiable conditions, each written as `- [ ] <criterion>`. Every item must be objectively checkable (a reviewer can say yes/no); avoid vague criteria like "works well". This section is REQUIRED and is the yardstick the review and (for non-code work) the deliverable-gate score against — make it complete.
8
+ - `## TL;DR` — a plain-English summary of what will be done and how it will be verified, at most 300 characters. This is the short answer the operator sees in the inbox to decide whether to approve; make it clear and honest, not marketing.
9
+ - One or more `### Requirement: <name>` sections. Each states, in SHALL language, one thing the finished work must do — what a "Problem"/"Scope" section used to describe as prose, stated instead as a testable rule. Cover what today's Problem/Scope/Non-goals sections would have covered: why the task exists, what changes, and what it explicitly does NOT do (state a non-goal as its own requirement, e.g. "### Requirement: Out of scope" with a SHALL NOT). Record any assumption you had to make inside the requirement it affects, prefixed `Assumption:`.
10
+ - Each requirement has one or more `#### Scenario: <name>` sub-sections, each a concrete situation using **WHEN** / **THEN** (and **AND** for additional conditions) describing exactly what happens.
11
+ - `## Acceptance criteria` — a markdown checklist of concrete, verifiable conditions, each written as `- [ ] <criterion>`. Every item must be objectively checkable (a reviewer can say yes/no); avoid vague criteria like "works well". This section is REQUIRED and is the yardstick the review and (for non-code work) the deliverable-gate score against — make it complete. It is the one section that survives unchanged from the old Problem/Scope/Non-goals template: the requirement/scenario body replaces prose description, not the checklist.
12
12
  - `## Verification` — how completion is proven: the exact tests, commands, or manual checks that demonstrate each acceptance criterion holds.
13
- - `## Коротко` — a plain-Ukrainian TL;DR of what will be done and how it will be verified, written for a non-engineer. HARD LIMIT: 300 characters. This is what the operator sees in the inbox to decide whether to approve; make it clear and honest, not marketing.
14
13
 
15
- Match the domain of the task: a code spec talks about code changes and tests; an seo/content/marketing spec talks about the deliverable, its audience, and how its quality is judged. Keep the spec tight — every sentence should help someone execute the task.
14
+ Match the domain of the task: a code spec's requirements talk about code changes and tests; an seo/content/marketing spec's requirements talk about the deliverable, its audience, and how its quality is judged. Keep the spec tight — every sentence should help someone execute the task.
15
+
16
+ **Size budget.** The authoring prompt states a `maxRequirements` and `maxChars` for this task's impact level. Stay within both: no more acceptance-criteria items than `maxRequirements`, and no more total characters (across the entire markdown) than `maxChars`. If the full spec would exceed either, slim it down rather than truncate mid-section.
17
+
18
+ ## Example
19
+
20
+ ```markdown
21
+ # GT-1234 — Add rate limiting to the webhook endpoint
22
+
23
+ ## TL;DR
24
+ Adds a 10 req/min cap per project on the webhook endpoint so one misconfigured
25
+ caller can't starve others; verified by a new test that trips the limit.
26
+
27
+ ### Requirement: Webhook requests are rate-limited per project
28
+
29
+ The webhook endpoint SHALL reject requests beyond 10 per minute for a given project.
30
+
31
+ #### Scenario: A project under the limit is unaffected
32
+ - **WHEN** a project has made fewer than 10 requests in the current minute
33
+ - **THEN** its next request is processed normally
34
+
35
+ #### Scenario: A project over the limit is rejected
36
+ - **WHEN** a project has already made 10 requests in the current minute
37
+ - **THEN** the next request receives a 429 response
38
+ - **AND** no webhook handler logic runs for that request
39
+
40
+ ### Requirement: Out of scope
41
+ This task SHALL NOT change rate limits for any endpoint other than the webhook endpoint.
42
+
43
+ ## Acceptance criteria
44
+ - [ ] A project's 11th request within a minute receives HTTP 429.
45
+ - [ ] A project's 10th request within a minute still succeeds.
46
+ - [ ] No other endpoint's behavior changes.
47
+
48
+ ## Verification
49
+ Run the new `webhook-rate-limit.test.ts` suite; it drives 11 requests in one minute
50
+ against a test project and asserts the 11th is a 429 while the first 10 are 200s.
51
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrlin-dev/mcp",
3
- "version": "0.3.59",
3
+ "version": "0.3.60",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mrrlin-mcp": "dist/bin.cjs"
@@ -22,8 +22,8 @@
22
22
  "esbuild": "^0.24.0",
23
23
  "tsx": "^4.22.3",
24
24
  "@mrrlin/director-e2e": "0.0.0",
25
- "@mrrlin/wiki": "0.0.0",
26
25
  "@mrrlin/schemas": "0.0.0",
26
+ "@mrrlin/wiki": "0.0.0",
27
27
  "@mrrlin/client": "0.0.0",
28
28
  "@mrrlin/codex-client": "0.0.0",
29
29
  "@mrrlin/tsconfig": "0.0.0"